mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-24 03:06:38 +00:00
Compare commits
67 Commits
poc-token-
...
test/proxy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95248e52f7 | ||
|
|
8e7b016be2 | ||
|
|
9e01ea7aae | ||
|
|
cfc7ec8bb9 | ||
|
|
b3bbc0e5c6 | ||
|
|
d7c8e37ff4 | ||
|
|
05b66e73bc | ||
|
|
01ceedac89 | ||
|
|
403babd433 | ||
|
|
47133031e5 | ||
|
|
82da606886 | ||
|
|
bbe5ae2145 | ||
|
|
0b21498b39 | ||
|
|
0ca59535f1 | ||
|
|
59c77d0658 | ||
|
|
333e045099 | ||
|
|
c2c4d9d336 | ||
|
|
9a6a72e88e | ||
|
|
afe6d9fca4 | ||
|
|
ef82905526 | ||
|
|
d18747e846 | ||
|
|
f341d69314 | ||
|
|
327142837c | ||
|
|
f8c0321aee | ||
|
|
89115ff76a | ||
|
|
63c83aa8d2 | ||
|
|
37f025c966 | ||
|
|
4a54f0d670 | ||
|
|
98890a29e3 | ||
|
|
9d123ec059 | ||
|
|
5d171f181a | ||
|
|
22f878b3b7 | ||
|
|
44ef1a18dd | ||
|
|
2b98dc4e52 | ||
|
|
2a26cb4567 | ||
|
|
5ca1b64328 | ||
|
|
36752a8cbb | ||
|
|
f117fc7509 | ||
|
|
fc6b93ae59 | ||
|
|
564fa4ab04 | ||
|
|
a6db88fbd2 | ||
|
|
4b5294e596 | ||
|
|
a322dce42a | ||
|
|
d1ead2265b | ||
|
|
bbca74476e | ||
|
|
318cf59d66 | ||
|
|
e9b2a6e808 | ||
|
|
2dbdb5c1a7 | ||
|
|
2cdab6d7b7 | ||
|
|
e49c0e8862 | ||
|
|
e7c84d0ead | ||
|
|
1c934cca64 | ||
|
|
4aff4a6424 | ||
|
|
1bd7190954 | ||
|
|
0146e39714 | ||
|
|
baed6e46ec | ||
|
|
0d1ffba75f | ||
|
|
1024d45698 | ||
|
|
e5d4947d60 | ||
|
|
cb9b39b950 | ||
|
|
68c481fa44 | ||
|
|
01a9cd4651 | ||
|
|
f53155562f | ||
|
|
edce11b34d | ||
|
|
841b2d26c6 | ||
|
|
d3eeb6d8ee | ||
|
|
7ebf37ef20 |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
|
*.p12
|
||||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Community Support
|
||||||
|
url: https://forum.netbird.io/
|
||||||
|
about: Community support forum
|
||||||
|
- name: Cloud Support
|
||||||
|
url: https://docs.netbird.io/help/report-bug-issues
|
||||||
|
about: Contact us for support
|
||||||
|
- name: Client/Connection Troubleshooting
|
||||||
|
url: https://docs.netbird.io/help/troubleshooting-client
|
||||||
|
about: See our client troubleshooting guide for help addressing common issues
|
||||||
|
- name: Self-host Troubleshooting
|
||||||
|
url: https://docs.netbird.io/selfhosted/troubleshooting
|
||||||
|
about: See our self-host troubleshooting guide for help addressing common issues
|
||||||
10
.github/workflows/check-license-dependencies.yml
vendored
10
.github/workflows/check-license-dependencies.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check for problematic license dependencies
|
- name: Check for problematic license dependencies
|
||||||
run: |
|
run: |
|
||||||
echo "Checking for dependencies on management/, signal/, and relay/ packages..."
|
echo "Checking for dependencies on management/, signal/, relay/, and proxy/ packages..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Find all directories except the problematic ones and system dirs
|
# Find all directories except the problematic ones and system dirs
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
while IFS= read -r dir; do
|
while IFS= read -r dir; do
|
||||||
echo "=== Checking $dir ==="
|
echo "=== Checking $dir ==="
|
||||||
# Search for problematic imports, excluding test files
|
# Search for problematic imports, excluding test files
|
||||||
RESULTS=$(grep -r "github.com/netbirdio/netbird/\(management\|signal\|relay\)" "$dir" --include="*.go" 2>/dev/null | grep -v "_test.go" | grep -v "test_" | grep -v "/test/" || true)
|
RESULTS=$(grep -r "github.com/netbirdio/netbird/\(management\|signal\|relay\|proxy\)" "$dir" --include="*.go" 2>/dev/null | grep -v "_test.go" | grep -v "test_" | grep -v "/test/" || true)
|
||||||
if [ -n "$RESULTS" ]; then
|
if [ -n "$RESULTS" ]; then
|
||||||
echo "❌ Found problematic dependencies:"
|
echo "❌ Found problematic dependencies:"
|
||||||
echo "$RESULTS"
|
echo "$RESULTS"
|
||||||
@@ -39,11 +39,11 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "✓ No problematic dependencies found"
|
echo "✓ No problematic dependencies found"
|
||||||
fi
|
fi
|
||||||
done < <(find . -maxdepth 1 -type d -not -name "." -not -name "management" -not -name "signal" -not -name "relay" -not -name ".git*" | sort)
|
done < <(find . -maxdepth 1 -type d -not -name "." -not -name "management" -not -name "signal" -not -name "relay" -not -name "proxy" -not -name "combined" -not -name ".git*" | sort)
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
if [ $FOUND_ISSUES -eq 1 ]; then
|
if [ $FOUND_ISSUES -eq 1 ]; then
|
||||||
echo "❌ Found dependencies on management/, signal/, or relay/ packages"
|
echo "❌ Found dependencies on management/, signal/, relay/, or proxy/ packages"
|
||||||
echo "These packages are licensed under AGPLv3 and must not be imported by BSD-licensed code"
|
echo "These packages are licensed under AGPLv3 and must not be imported by BSD-licensed code"
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
IMPORTERS=$(go list -json -deps ./... 2>/dev/null | jq -r "select(.Imports[]? == \"$package\") | .ImportPath")
|
IMPORTERS=$(go list -json -deps ./... 2>/dev/null | jq -r "select(.Imports[]? == \"$package\") | .ImportPath")
|
||||||
|
|
||||||
# Check if any importer is NOT in management/signal/relay
|
# Check if any importer is NOT in management/signal/relay
|
||||||
BSD_IMPORTER=$(echo "$IMPORTERS" | grep -v "github.com/netbirdio/netbird/\(management\|signal\|relay\)" | head -1)
|
BSD_IMPORTER=$(echo "$IMPORTERS" | grep -v "github.com/netbirdio/netbird/\(management\|signal\|relay\|proxy\|combined\)" | head -1)
|
||||||
|
|
||||||
if [ -n "$BSD_IMPORTER" ]; then
|
if [ -n "$BSD_IMPORTER" ]; then
|
||||||
echo "❌ $package ($license) is imported by BSD-licensed code: $BSD_IMPORTER"
|
echo "❌ $package ($license) is imported by BSD-licensed code: $BSD_IMPORTER"
|
||||||
|
|||||||
2
.github/workflows/golang-test-darwin.yml
vendored
2
.github/workflows/golang-test-darwin.yml
vendored
@@ -43,5 +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 -tags=devcert -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 $(go list ./... | grep -v /management)
|
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)
|
||||||
|
|
||||||
|
|||||||
1
.github/workflows/golang-test-freebsd.yml
vendored
1
.github/workflows/golang-test-freebsd.yml
vendored
@@ -46,6 +46,5 @@ jobs:
|
|||||||
time go test -timeout 1m -failfast ./client/iface/...
|
time go test -timeout 1m -failfast ./client/iface/...
|
||||||
time go test -timeout 1m -failfast ./route/...
|
time go test -timeout 1m -failfast ./route/...
|
||||||
time go test -timeout 1m -failfast ./sharedsock/...
|
time go test -timeout 1m -failfast ./sharedsock/...
|
||||||
time go test -timeout 1m -failfast ./signal/...
|
|
||||||
time go test -timeout 1m -failfast ./util/...
|
time go test -timeout 1m -failfast ./util/...
|
||||||
time go test -timeout 1m -failfast ./version/...
|
time go test -timeout 1m -failfast ./version/...
|
||||||
|
|||||||
98
.github/workflows/golang-test-linux.yml
vendored
98
.github/workflows/golang-test-linux.yml
vendored
@@ -97,6 +97,16 @@ jobs:
|
|||||||
working-directory: relay
|
working-directory: relay
|
||||||
run: CGO_ENABLED=1 GOARCH=386 go build -o relay-386 .
|
run: CGO_ENABLED=1 GOARCH=386 go build -o relay-386 .
|
||||||
|
|
||||||
|
- name: Build combined
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
working-directory: combined
|
||||||
|
run: CGO_ENABLED=1 go build .
|
||||||
|
|
||||||
|
- name: Build combined 386
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
working-directory: combined
|
||||||
|
run: CGO_ENABLED=1 GOARCH=386 go build -o combined-386 .
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: "Client / Unit"
|
name: "Client / Unit"
|
||||||
needs: [build-cache]
|
needs: [build-cache]
|
||||||
@@ -144,7 +154,7 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay)
|
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)
|
||||||
|
|
||||||
test_client_on_docker:
|
test_client_on_docker:
|
||||||
name: "Client (Docker) / Unit"
|
name: "Client (Docker) / Unit"
|
||||||
@@ -204,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 -v -timeout 10m -p 1 $(go list -buildvcs=false ./... | grep -v -e /management -e /signal -e /relay -e /client/ui -e /upload-server)
|
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:
|
||||||
@@ -261,6 +271,53 @@ jobs:
|
|||||||
-exec 'sudo' \
|
-exec 'sudo' \
|
||||||
-timeout 10m -p 1 ./relay/... ./shared/relay/...
|
-timeout 10m -p 1 ./relay/... ./shared/relay/...
|
||||||
|
|
||||||
|
test_proxy:
|
||||||
|
name: "Proxy / Unit"
|
||||||
|
needs: [build-cache]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [ '386','amd64' ]
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: "go.mod"
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y gcc-multilib g++-multilib libc6-dev-i386
|
||||||
|
|
||||||
|
- name: Get Go environment
|
||||||
|
run: |
|
||||||
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ env.cache }}
|
||||||
|
${{ env.modcache }}
|
||||||
|
key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gotest-cache-
|
||||||
|
|
||||||
|
- name: Install modules
|
||||||
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: check git status
|
||||||
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
|
go test -timeout 10m -p 1 ./proxy/...
|
||||||
|
|
||||||
test_signal:
|
test_signal:
|
||||||
name: "Signal / Unit"
|
name: "Signal / Unit"
|
||||||
needs: [build-cache]
|
needs: [build-cache]
|
||||||
@@ -352,12 +409,19 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: matrix.store == 'mysql' && (github.repository == github.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@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
|
- name: docker login for root user
|
||||||
|
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
|
||||||
|
env:
|
||||||
|
DOCKER_USER: ${{ secrets.DOCKER_USER }}
|
||||||
|
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
run: echo "$DOCKER_TOKEN" | sudo docker login --username "$DOCKER_USER" --password-stdin
|
||||||
|
|
||||||
- name: download mysql image
|
- name: download mysql image
|
||||||
if: matrix.store == 'mysql'
|
if: matrix.store == 'mysql'
|
||||||
run: docker pull mlsmaycon/warmed-mysql:8
|
run: docker pull mlsmaycon/warmed-mysql:8
|
||||||
@@ -440,15 +504,18 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: matrix.store == 'mysql' && (github.repository == github.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@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
- name: download mysql image
|
- name: docker login for root user
|
||||||
if: matrix.store == 'mysql'
|
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
|
||||||
run: docker pull mlsmaycon/warmed-mysql:8
|
env:
|
||||||
|
DOCKER_USER: ${{ secrets.DOCKER_USER }}
|
||||||
|
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
run: echo "$DOCKER_TOKEN" | sudo docker login --username "$DOCKER_USER" --password-stdin
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
@@ -529,15 +596,18 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: matrix.store == 'mysql' && (github.repository == github.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@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
- name: download mysql image
|
- name: docker login for root user
|
||||||
if: matrix.store == 'mysql'
|
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
|
||||||
run: docker pull mlsmaycon/warmed-mysql:8
|
env:
|
||||||
|
DOCKER_USER: ${{ secrets.DOCKER_USER }}
|
||||||
|
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
run: echo "$DOCKER_TOKEN" | sudo docker login --username "$DOCKER_USER" --password-stdin
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.github/workflows/golang-test-windows.yml
vendored
2
.github/workflows/golang-test-windows.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
|||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=${{ env.cache }}
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=${{ env.cache }}
|
||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=${{ env.modcache }}
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=${{ env.modcache }}
|
||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe mod tidy
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe mod tidy
|
||||||
- run: echo "files=$(go list ./... | ForEach-Object { $_ } | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' })" >> $env:GITHUB_ENV
|
- run: echo "files=$(go list ./... | ForEach-Object { $_ } | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' } | Where-Object { $_ -notmatch '/proxy' } | Where-Object { $_ -notmatch '/combined' })" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -tags=devcert -timeout 10m -p 1 ${{ env.files }} > test-out.txt 2>&1"
|
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -tags=devcert -timeout 10m -p 1 ${{ env.files }} > test-out.txt 2>&1"
|
||||||
|
|||||||
4
.github/workflows/golangci-lint.yml
vendored
4
.github/workflows/golangci-lint.yml
vendored
@@ -19,8 +19,8 @@ jobs:
|
|||||||
- name: codespell
|
- name: codespell
|
||||||
uses: codespell-project/actions-codespell@v2
|
uses: codespell-project/actions-codespell@v2
|
||||||
with:
|
with:
|
||||||
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros,ans
|
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros,ans,deriver
|
||||||
skip: go.mod,go.sum
|
skip: go.mod,go.sum,**/proxy/web/**
|
||||||
golangci:
|
golangci:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|||||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -160,7 +160,7 @@ jobs:
|
|||||||
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'
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -176,6 +176,7 @@ jobs:
|
|||||||
- name: Generate windows syso arm64
|
- name: Generate windows syso arm64
|
||||||
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
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
@@ -185,6 +186,19 @@ jobs:
|
|||||||
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||||
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
|
- name: Tag and push PR images (amd64 only)
|
||||||
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
run: |
|
||||||
|
PR_TAG="pr-${{ github.event.pull_request.number }}"
|
||||||
|
echo '${{ steps.goreleaser.outputs.artifacts }}' | \
|
||||||
|
jq -r '.[] | select(.type == "Docker Image") | select(.goarch == "amd64") | .name' | \
|
||||||
|
grep '^ghcr.io/' | while read -r SRC; do
|
||||||
|
IMG_NAME="${SRC%%:*}"
|
||||||
|
DST="${IMG_NAME}:${PR_TAG}"
|
||||||
|
echo "Tagging ${SRC} -> ${DST}"
|
||||||
|
docker tag "$SRC" "$DST"
|
||||||
|
docker push "$DST"
|
||||||
|
done
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
.run
|
.run
|
||||||
*.iml
|
*.iml
|
||||||
dist/
|
dist/
|
||||||
|
!proxy/web/dist/
|
||||||
bin/
|
bin/
|
||||||
.env
|
.env
|
||||||
conf.json
|
conf.json
|
||||||
|
|||||||
@@ -140,6 +140,20 @@ builds:
|
|||||||
- -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 }}"
|
||||||
|
|
||||||
|
- id: netbird-proxy
|
||||||
|
dir: proxy/cmd/proxy
|
||||||
|
env: [CGO_ENABLED=0]
|
||||||
|
binary: netbird-proxy
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- arm
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X main.Version={{.Version}} -X main.Commit={{.Commit}} -X main.BuildDate={{.CommitDate}}
|
||||||
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
|
||||||
universal_binaries:
|
universal_binaries:
|
||||||
- id: netbird
|
- id: netbird
|
||||||
|
|
||||||
@@ -589,6 +603,55 @@ dockers:
|
|||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
- "--label=maintainer=dev@netbird.io"
|
- "--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:
|
docker_manifests:
|
||||||
- name_template: netbirdio/netbird:{{ .Version }}
|
- name_template: netbirdio/netbird:{{ .Version }}
|
||||||
image_templates:
|
image_templates:
|
||||||
@@ -769,6 +832,30 @@ docker_manifests:
|
|||||||
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-amd64
|
- 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
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
This BSD‑3‑Clause license applies to all parts of the repository except for the directories management/, signal/ and relay/.
|
This BSD‑3‑Clause license applies to all parts of the repository except for the directories management/, signal/, relay/ and combined/.
|
||||||
Those directories are licensed under the GNU Affero General Public License version 3.0 (AGPLv3). See the respective LICENSE files inside each directory.
|
Those directories are licensed under the GNU Affero General Public License version 3.0 (AGPLv3). See the respective LICENSE files inside each directory.
|
||||||
|
|
||||||
BSD 3-Clause License
|
BSD 3-Clause License
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# sudo podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
|
# sudo podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
|
||||||
# sudo podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
|
# sudo podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
|
||||||
|
|
||||||
FROM alpine:3.23.2
|
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 \
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
package android
|
package android
|
||||||
|
|
||||||
import "github.com/netbirdio/netbird/client/internal/peer"
|
import (
|
||||||
|
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// EnvKeyNBForceRelay Exported for Android java client
|
// EnvKeyNBForceRelay Exported for Android java client to force relay connections
|
||||||
EnvKeyNBForceRelay = peer.EnvKeyNBForceRelay
|
EnvKeyNBForceRelay = peer.EnvKeyNBForceRelay
|
||||||
|
|
||||||
|
// EnvKeyNBLazyConn Exported for Android java client to configure lazy connection
|
||||||
|
EnvKeyNBLazyConn = lazyconn.EnvEnableLazyConn
|
||||||
|
|
||||||
|
// EnvKeyNBInactivityThreshold Exported for Android java client to configure connection inactivity threshold
|
||||||
|
EnvKeyNBInactivityThreshold = lazyconn.EnvInactivityThreshold
|
||||||
)
|
)
|
||||||
|
|
||||||
// EnvList wraps a Go map for export to Java
|
// EnvList wraps a Go map for export to Java
|
||||||
|
|||||||
194
client/cmd/expose.go
Normal file
194
client/cmd/expose.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pinRegexp = regexp.MustCompile(`^\d{6}$`)
|
||||||
|
|
||||||
|
var (
|
||||||
|
exposePin string
|
||||||
|
exposePassword string
|
||||||
|
exposeUserGroups []string
|
||||||
|
exposeDomain string
|
||||||
|
exposeNamePrefix string
|
||||||
|
exposeProtocol string
|
||||||
|
)
|
||||||
|
|
||||||
|
var exposeCmd = &cobra.Command{
|
||||||
|
Use: "expose <port>",
|
||||||
|
Short: "Expose a local port via the NetBird reverse proxy",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Example: "netbird expose --with-password safe-pass 8080",
|
||||||
|
RunE: exposeFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
exposeCmd.Flags().StringVar(&exposePin, "with-pin", "", "Protect the exposed service with a 6-digit PIN (e.g. --with-pin 123456)")
|
||||||
|
exposeCmd.Flags().StringVar(&exposePassword, "with-password", "", "Protect the exposed service with a password (e.g. --with-password my-secret)")
|
||||||
|
exposeCmd.Flags().StringSliceVar(&exposeUserGroups, "with-user-groups", nil, "Restrict access to specific user groups with SSO (e.g. --with-user-groups devops,Backend)")
|
||||||
|
exposeCmd.Flags().StringVar(&exposeDomain, "with-custom-domain", "", "Custom domain for the exposed service, must be configured to your account (e.g. --with-custom-domain myapp.example.com)")
|
||||||
|
exposeCmd.Flags().StringVar(&exposeNamePrefix, "with-name-prefix", "", "Prefix for the generated service name (e.g. --with-name-prefix my-app)")
|
||||||
|
exposeCmd.Flags().StringVar(&exposeProtocol, "protocol", "http", "Protocol to use, http/https is supported (e.g. --protocol http)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateExposeFlags(cmd *cobra.Command, portStr string) (uint64, error) {
|
||||||
|
port, err := strconv.ParseUint(portStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid port number: %s", portStr)
|
||||||
|
}
|
||||||
|
if port == 0 || port > 65535 {
|
||||||
|
return 0, fmt.Errorf("invalid port number: must be between 1 and 65535")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isProtocolValid(exposeProtocol) {
|
||||||
|
return 0, fmt.Errorf("unsupported protocol %q: only 'http' or 'https' are supported", exposeProtocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exposePin != "" && !pinRegexp.MatchString(exposePin) {
|
||||||
|
return 0, fmt.Errorf("invalid pin: must be exactly 6 digits")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flags().Changed("with-password") && exposePassword == "" {
|
||||||
|
return 0, fmt.Errorf("password cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flags().Changed("with-user-groups") && len(exposeUserGroups) == 0 {
|
||||||
|
return 0, fmt.Errorf("user groups cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return port, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isProtocolValid(exposeProtocol string) bool {
|
||||||
|
return strings.ToLower(exposeProtocol) == "http" || strings.ToLower(exposeProtocol) == "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
func exposeFn(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars(rootCmd)
|
||||||
|
|
||||||
|
if err := util.InitLog(logLevel, util.LogConsole); err != nil {
|
||||||
|
log.Errorf("failed initializing log %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Root().SilenceUsage = false
|
||||||
|
|
||||||
|
port, err := validateExposeFlags(cmd, args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Root().SilenceUsage = true
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-sigCh
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to daemon: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Debugf("failed to close daemon connection: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
|
protocol, err := toExposeProtocol(exposeProtocol)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stream, err := client.ExposeService(ctx, &proto.ExposeServiceRequest{
|
||||||
|
Port: uint32(port),
|
||||||
|
Protocol: protocol,
|
||||||
|
Pin: exposePin,
|
||||||
|
Password: exposePassword,
|
||||||
|
UserGroups: exposeUserGroups,
|
||||||
|
Domain: exposeDomain,
|
||||||
|
NamePrefix: exposeNamePrefix,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("expose service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handleExposeReady(cmd, stream, port); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return waitForExposeEvents(cmd, ctx, stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toExposeProtocol(exposeProtocol string) (proto.ExposeProtocol, error) {
|
||||||
|
switch strings.ToLower(exposeProtocol) {
|
||||||
|
case "http":
|
||||||
|
return proto.ExposeProtocol_EXPOSE_HTTP, nil
|
||||||
|
case "https":
|
||||||
|
return proto.ExposeProtocol_EXPOSE_HTTPS, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unsupported protocol %q: only 'http' or 'https' are supported", exposeProtocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleExposeReady(cmd *cobra.Command, stream proto.DaemonService_ExposeServiceClient, port uint64) error {
|
||||||
|
event, err := stream.Recv()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("receive expose event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e := event.Event.(type) {
|
||||||
|
case *proto.ExposeServiceEvent_Ready:
|
||||||
|
cmd.Println("Service exposed successfully!")
|
||||||
|
cmd.Printf(" Name: %s\n", e.Ready.ServiceName)
|
||||||
|
cmd.Printf(" URL: %s\n", e.Ready.ServiceUrl)
|
||||||
|
cmd.Printf(" Domain: %s\n", e.Ready.Domain)
|
||||||
|
cmd.Printf(" Protocol: %s\n", exposeProtocol)
|
||||||
|
cmd.Printf(" Port: %d\n", port)
|
||||||
|
cmd.Println()
|
||||||
|
cmd.Println("Press Ctrl+C to stop exposing.")
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected expose event: %T", event.Event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForExposeEvents(cmd *cobra.Command, ctx context.Context, stream proto.DaemonService_ExposeServiceClient) error {
|
||||||
|
for {
|
||||||
|
_, err := stream.Recv()
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
cmd.Println("\nService stopped.")
|
||||||
|
//nolint:nilerr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return fmt.Errorf("connection to daemon closed unexpectedly")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("stream error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
|
||||||
|
daddr "github.com/netbirdio/netbird/client/internal/daemonaddr"
|
||||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,6 +81,15 @@ var (
|
|||||||
Short: "",
|
Short: "",
|
||||||
Long: "",
|
Long: "",
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars(cmd.Root())
|
||||||
|
|
||||||
|
// Don't resolve for service commands — they create the socket, not connect to it.
|
||||||
|
if !isServiceCmd(cmd) {
|
||||||
|
daemonAddr = daddr.ResolveUnixDaemonAddr(daemonAddr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -144,6 +154,7 @@ func init() {
|
|||||||
rootCmd.AddCommand(forwardingRulesCmd)
|
rootCmd.AddCommand(forwardingRulesCmd)
|
||||||
rootCmd.AddCommand(debugCmd)
|
rootCmd.AddCommand(debugCmd)
|
||||||
rootCmd.AddCommand(profileCmd)
|
rootCmd.AddCommand(profileCmd)
|
||||||
|
rootCmd.AddCommand(exposeCmd)
|
||||||
|
|
||||||
networksCMD.AddCommand(routesListCmd)
|
networksCMD.AddCommand(routesListCmd)
|
||||||
networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd)
|
networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd)
|
||||||
@@ -385,7 +396,6 @@ func migrateToNetbird(oldPath, newPath string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getClient(cmd *cobra.Command) (*grpc.ClientConn, error) {
|
func getClient(cmd *cobra.Command) (*grpc.ClientConn, error) {
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
|
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
|
||||||
@@ -398,3 +408,13 @@ func getClient(cmd *cobra.Command) (*grpc.ClientConn, error) {
|
|||||||
|
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isServiceCmd returns true if cmd is the "service" command or a child of it.
|
||||||
|
func isServiceCmd(cmd *cobra.Command) bool {
|
||||||
|
for c := cmd; c != nil; c = c.Parent() {
|
||||||
|
if c.Name() == "service" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ var (
|
|||||||
ErrConfigNotInitialized = errors.New("config not initialized")
|
ErrConfigNotInitialized = errors.New("config not initialized")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PeerConnStatus is a peer's connection status.
|
||||||
|
type PeerConnStatus = peer.ConnStatus
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PeerStatusConnected indicates the peer is in connected state.
|
||||||
|
PeerStatusConnected = peer.StatusConnected
|
||||||
|
)
|
||||||
|
|
||||||
// Client manages a netbird embedded client instance.
|
// Client manages a netbird embedded client instance.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
deviceName string
|
deviceName string
|
||||||
@@ -162,6 +170,7 @@ func New(opts Options) (*Client, error) {
|
|||||||
setupKey: opts.SetupKey,
|
setupKey: opts.SetupKey,
|
||||||
jwtToken: opts.JWTToken,
|
jwtToken: opts.JWTToken,
|
||||||
config: config,
|
config: config,
|
||||||
|
recorder: peer.NewRecorder(config.ManagementURL.String()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +192,7 @@ func (c *Client) Start(startCtx context.Context) error {
|
|||||||
|
|
||||||
// nolint:staticcheck
|
// nolint:staticcheck
|
||||||
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, c.deviceName)
|
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, c.deviceName)
|
||||||
|
|
||||||
authClient, err := auth.NewAuth(ctx, c.config.PrivateKey, c.config.ManagementURL, c.config)
|
authClient, err := auth.NewAuth(ctx, c.config.PrivateKey, c.config.ManagementURL, c.config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create auth client: %w", err)
|
return fmt.Errorf("create auth client: %w", err)
|
||||||
@@ -192,10 +202,7 @@ func (c *Client) Start(startCtx context.Context) error {
|
|||||||
if err, _ := authClient.Login(ctx, c.setupKey, c.jwtToken); err != nil {
|
if err, _ := authClient.Login(ctx, c.setupKey, c.jwtToken); err != nil {
|
||||||
return fmt.Errorf("login: %w", err)
|
return fmt.Errorf("login: %w", err)
|
||||||
}
|
}
|
||||||
|
client := internal.NewConnectClient(ctx, c.config, c.recorder, false)
|
||||||
recorder := peer.NewRecorder(c.config.ManagementURL.String())
|
|
||||||
c.recorder = recorder
|
|
||||||
client := internal.NewConnectClient(ctx, c.config, recorder, false)
|
|
||||||
client.SetSyncResponsePersistence(true)
|
client.SetSyncResponsePersistence(true)
|
||||||
|
|
||||||
// either startup error (permanent backoff err) or nil err (successful engine up)
|
// either startup error (permanent backoff err) or nil err (successful engine up)
|
||||||
@@ -348,14 +355,9 @@ func (c *Client) NewHTTPClient() *http.Client {
|
|||||||
// 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()
|
||||||
recorder := c.recorder
|
|
||||||
connect := c.connect
|
connect := c.connect
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
if recorder == nil {
|
|
||||||
return peer.FullStatus{}, errors.New("client not started")
|
|
||||||
}
|
|
||||||
|
|
||||||
if connect != nil {
|
if connect != nil {
|
||||||
engine := connect.Engine()
|
engine := connect.Engine()
|
||||||
if engine != nil {
|
if engine != nil {
|
||||||
@@ -363,7 +365,7 @@ func (c *Client) Status() (peer.FullStatus, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return recorder.GetFullStatus(), nil
|
return c.recorder.GetFullStatus(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLatestSyncResponse returns the latest sync response from the management server.
|
// GetLatestSyncResponse returns the latest sync response from the management server.
|
||||||
|
|||||||
@@ -115,6 +115,17 @@ func (t *TCPConnTrack) IsTombstone() bool {
|
|||||||
return t.tombstone.Load()
|
return t.tombstone.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSupersededBy returns true if this connection should be replaced by a new one
|
||||||
|
// carrying the given flags. Tombstoned connections are always superseded; TIME-WAIT
|
||||||
|
// connections are superseded by a pure SYN (a new connection attempt for the same
|
||||||
|
// four-tuple, as contemplated by RFC 1122 §4.2.2.13 and RFC 6191).
|
||||||
|
func (t *TCPConnTrack) IsSupersededBy(flags uint8) bool {
|
||||||
|
if t.tombstone.Load() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return flags&TCPSyn != 0 && flags&TCPAck == 0 && TCPState(t.state.Load()) == TCPStateTimeWait
|
||||||
|
}
|
||||||
|
|
||||||
// SetTombstone safely marks the connection for deletion
|
// SetTombstone safely marks the connection for deletion
|
||||||
func (t *TCPConnTrack) SetTombstone() {
|
func (t *TCPConnTrack) SetTombstone() {
|
||||||
t.tombstone.Store(true)
|
t.tombstone.Store(true)
|
||||||
@@ -169,7 +180,7 @@ func (t *TCPTracker) updateIfExists(srcIP, dstIP netip.Addr, srcPort, dstPort ui
|
|||||||
conn, exists := t.connections[key]
|
conn, exists := t.connections[key]
|
||||||
t.mutex.RUnlock()
|
t.mutex.RUnlock()
|
||||||
|
|
||||||
if exists {
|
if exists && !conn.IsSupersededBy(flags) {
|
||||||
t.updateState(key, conn, flags, direction, size)
|
t.updateState(key, conn, flags, direction, size)
|
||||||
return key, uint16(conn.DNATOrigPort.Load()), true
|
return key, uint16(conn.DNATOrigPort.Load()), true
|
||||||
}
|
}
|
||||||
@@ -241,7 +252,7 @@ func (t *TCPTracker) IsValidInbound(srcIP, dstIP netip.Addr, srcPort, dstPort ui
|
|||||||
conn, exists := t.connections[key]
|
conn, exists := t.connections[key]
|
||||||
t.mutex.RUnlock()
|
t.mutex.RUnlock()
|
||||||
|
|
||||||
if !exists || conn.IsTombstone() {
|
if !exists || conn.IsSupersededBy(flags) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -485,6 +485,261 @@ func TestTCPAbnormalSequences(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestTCPPortReuseTombstone verifies that a new connection on a port with a
|
||||||
|
// tombstoned (closed) conntrack entry is properly tracked. Without the fix,
|
||||||
|
// updateIfExists treats tombstoned entries as live, causing track() to skip
|
||||||
|
// creating a new connection. The subsequent SYN-ACK then fails IsValidInbound
|
||||||
|
// because the entry is tombstoned, and the response packet gets dropped by ACL.
|
||||||
|
func TestTCPPortReuseTombstone(t *testing.T) {
|
||||||
|
srcIP := netip.MustParseAddr("100.64.0.1")
|
||||||
|
dstIP := netip.MustParseAddr("100.64.0.2")
|
||||||
|
srcPort := uint16(12345)
|
||||||
|
dstPort := uint16(80)
|
||||||
|
|
||||||
|
t.Run("Outbound port reuse after graceful close", func(t *testing.T) {
|
||||||
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger)
|
||||||
|
defer tracker.Close()
|
||||||
|
|
||||||
|
key := ConnKey{SrcIP: srcIP, DstIP: dstIP, SrcPort: srcPort, DstPort: dstPort}
|
||||||
|
|
||||||
|
// Establish and gracefully close a connection (server-initiated close)
|
||||||
|
establishConnection(t, tracker, srcIP, dstIP, srcPort, dstPort)
|
||||||
|
|
||||||
|
// Server sends FIN
|
||||||
|
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck, 0)
|
||||||
|
require.True(t, valid)
|
||||||
|
|
||||||
|
// Client sends FIN-ACK
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck, 0)
|
||||||
|
|
||||||
|
// Server sends final ACK
|
||||||
|
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, 0)
|
||||||
|
require.True(t, valid)
|
||||||
|
|
||||||
|
// Connection should be tombstoned
|
||||||
|
conn := tracker.connections[key]
|
||||||
|
require.NotNil(t, conn, "old connection should still be in map")
|
||||||
|
require.True(t, conn.IsTombstone(), "old connection should be tombstoned")
|
||||||
|
|
||||||
|
// Now reuse the same port for a new connection
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn, 100)
|
||||||
|
|
||||||
|
// The old tombstoned entry should be replaced with a new one
|
||||||
|
newConn := tracker.connections[key]
|
||||||
|
require.NotNil(t, newConn, "new connection should exist")
|
||||||
|
require.False(t, newConn.IsTombstone(), "new connection should not be tombstoned")
|
||||||
|
require.Equal(t, TCPStateSynSent, newConn.GetState())
|
||||||
|
|
||||||
|
// SYN-ACK for the new connection should be valid
|
||||||
|
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck, 100)
|
||||||
|
require.True(t, valid, "SYN-ACK for new connection on reused port should be accepted")
|
||||||
|
require.Equal(t, TCPStateEstablished, newConn.GetState())
|
||||||
|
|
||||||
|
// Data transfer should work
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, 100)
|
||||||
|
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPPush|TCPAck, 500)
|
||||||
|
require.True(t, valid, "data should be allowed on new connection")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Outbound port reuse after RST", func(t *testing.T) {
|
||||||
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger)
|
||||||
|
defer tracker.Close()
|
||||||
|
|
||||||
|
key := ConnKey{SrcIP: srcIP, DstIP: dstIP, SrcPort: srcPort, DstPort: dstPort}
|
||||||
|
|
||||||
|
// Establish and RST a connection
|
||||||
|
establishConnection(t, tracker, srcIP, dstIP, srcPort, dstPort)
|
||||||
|
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst|TCPAck, 0)
|
||||||
|
require.True(t, valid)
|
||||||
|
|
||||||
|
conn := tracker.connections[key]
|
||||||
|
require.True(t, conn.IsTombstone(), "RST connection should be tombstoned")
|
||||||
|
|
||||||
|
// Reuse the same port
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn, 100)
|
||||||
|
|
||||||
|
newConn := tracker.connections[key]
|
||||||
|
require.NotNil(t, newConn)
|
||||||
|
require.False(t, newConn.IsTombstone())
|
||||||
|
require.Equal(t, TCPStateSynSent, newConn.GetState())
|
||||||
|
|
||||||
|
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck, 100)
|
||||||
|
require.True(t, valid, "SYN-ACK should be accepted after RST tombstone")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Inbound port reuse after close", func(t *testing.T) {
|
||||||
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger)
|
||||||
|
defer tracker.Close()
|
||||||
|
|
||||||
|
clientIP := srcIP
|
||||||
|
serverIP := dstIP
|
||||||
|
clientPort := srcPort
|
||||||
|
serverPort := dstPort
|
||||||
|
key := ConnKey{SrcIP: clientIP, DstIP: serverIP, SrcPort: clientPort, DstPort: serverPort}
|
||||||
|
|
||||||
|
// Inbound connection: client SYN → server SYN-ACK → client ACK
|
||||||
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPSyn, nil, 100, 0)
|
||||||
|
tracker.TrackOutbound(serverIP, clientIP, serverPort, clientPort, TCPSyn|TCPAck, 100)
|
||||||
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPAck, nil, 100, 0)
|
||||||
|
|
||||||
|
conn := tracker.connections[key]
|
||||||
|
require.Equal(t, TCPStateEstablished, conn.GetState())
|
||||||
|
|
||||||
|
// Server-initiated close to reach Closed/tombstoned:
|
||||||
|
// Server FIN (opposite dir) → CloseWait
|
||||||
|
tracker.TrackOutbound(serverIP, clientIP, serverPort, clientPort, TCPFin|TCPAck, 100)
|
||||||
|
require.Equal(t, TCPStateCloseWait, conn.GetState())
|
||||||
|
// Client FIN-ACK (same dir as conn) → LastAck
|
||||||
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPFin|TCPAck, nil, 100, 0)
|
||||||
|
require.Equal(t, TCPStateLastAck, conn.GetState())
|
||||||
|
// Server final ACK (opposite dir) → Closed → tombstoned
|
||||||
|
tracker.TrackOutbound(serverIP, clientIP, serverPort, clientPort, TCPAck, 100)
|
||||||
|
|
||||||
|
require.True(t, conn.IsTombstone())
|
||||||
|
|
||||||
|
// New inbound connection on same ports
|
||||||
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPSyn, nil, 100, 0)
|
||||||
|
|
||||||
|
newConn := tracker.connections[key]
|
||||||
|
require.NotNil(t, newConn)
|
||||||
|
require.False(t, newConn.IsTombstone())
|
||||||
|
require.Equal(t, TCPStateSynReceived, newConn.GetState())
|
||||||
|
|
||||||
|
// Complete handshake: server SYN-ACK, then client ACK
|
||||||
|
tracker.TrackOutbound(serverIP, clientIP, serverPort, clientPort, TCPSyn|TCPAck, 100)
|
||||||
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPAck, nil, 100, 0)
|
||||||
|
require.Equal(t, TCPStateEstablished, newConn.GetState())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Late ACK on tombstoned connection is harmless", func(t *testing.T) {
|
||||||
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger)
|
||||||
|
defer tracker.Close()
|
||||||
|
|
||||||
|
key := ConnKey{SrcIP: srcIP, DstIP: dstIP, SrcPort: srcPort, DstPort: dstPort}
|
||||||
|
|
||||||
|
// Establish and close via passive close (server-initiated FIN → Closed → tombstoned)
|
||||||
|
establishConnection(t, tracker, srcIP, dstIP, srcPort, dstPort)
|
||||||
|
tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck, 0) // CloseWait
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck, 0) // LastAck
|
||||||
|
tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, 0) // Closed
|
||||||
|
|
||||||
|
conn := tracker.connections[key]
|
||||||
|
require.True(t, conn.IsTombstone())
|
||||||
|
|
||||||
|
// Late ACK should be rejected (tombstoned)
|
||||||
|
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, 0)
|
||||||
|
require.False(t, valid, "late ACK on tombstoned connection should be rejected")
|
||||||
|
|
||||||
|
// Late outbound ACK should not create a new connection (not a SYN)
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, 0)
|
||||||
|
require.True(t, tracker.connections[key].IsTombstone(), "late outbound ACK should not replace tombstoned entry")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTCPPortReuseTimeWait(t *testing.T) {
|
||||||
|
srcIP := netip.MustParseAddr("100.64.0.1")
|
||||||
|
dstIP := netip.MustParseAddr("100.64.0.2")
|
||||||
|
srcPort := uint16(12345)
|
||||||
|
dstPort := uint16(80)
|
||||||
|
|
||||||
|
t.Run("Outbound port reuse during TIME-WAIT (active close)", func(t *testing.T) {
|
||||||
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger)
|
||||||
|
defer tracker.Close()
|
||||||
|
|
||||||
|
key := ConnKey{SrcIP: srcIP, DstIP: dstIP, SrcPort: srcPort, DstPort: dstPort}
|
||||||
|
|
||||||
|
// Establish connection
|
||||||
|
establishConnection(t, tracker, srcIP, dstIP, srcPort, dstPort)
|
||||||
|
|
||||||
|
// Active close: client (outbound initiator) sends FIN first
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck, 0)
|
||||||
|
conn := tracker.connections[key]
|
||||||
|
require.Equal(t, TCPStateFinWait1, conn.GetState())
|
||||||
|
|
||||||
|
// Server ACKs the FIN
|
||||||
|
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, 0)
|
||||||
|
require.True(t, valid)
|
||||||
|
require.Equal(t, TCPStateFinWait2, conn.GetState())
|
||||||
|
|
||||||
|
// Server sends its own FIN
|
||||||
|
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck, 0)
|
||||||
|
require.True(t, valid)
|
||||||
|
require.Equal(t, TCPStateTimeWait, conn.GetState())
|
||||||
|
|
||||||
|
// Client sends final ACK (TIME-WAIT stays, not tombstoned)
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, 0)
|
||||||
|
require.False(t, conn.IsTombstone(), "TIME-WAIT should not be tombstoned")
|
||||||
|
|
||||||
|
// New outbound SYN on the same port (port reuse during TIME-WAIT)
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn, 100)
|
||||||
|
|
||||||
|
// Per RFC 1122/6191, new SYN during TIME-WAIT should start a new connection
|
||||||
|
newConn := tracker.connections[key]
|
||||||
|
require.NotNil(t, newConn, "new connection should exist")
|
||||||
|
require.False(t, newConn.IsTombstone(), "new connection should not be tombstoned")
|
||||||
|
require.Equal(t, TCPStateSynSent, newConn.GetState(), "new connection should be in SYN-SENT")
|
||||||
|
|
||||||
|
// SYN-ACK for new connection should be valid
|
||||||
|
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck, 100)
|
||||||
|
require.True(t, valid, "SYN-ACK for new connection should be accepted")
|
||||||
|
require.Equal(t, TCPStateEstablished, newConn.GetState())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Inbound SYN during TIME-WAIT falls through to normal tracking", func(t *testing.T) {
|
||||||
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger)
|
||||||
|
defer tracker.Close()
|
||||||
|
|
||||||
|
key := ConnKey{SrcIP: srcIP, DstIP: dstIP, SrcPort: srcPort, DstPort: dstPort}
|
||||||
|
|
||||||
|
// Establish outbound connection and close via active close → TIME-WAIT
|
||||||
|
establishConnection(t, tracker, srcIP, dstIP, srcPort, dstPort)
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck, 0)
|
||||||
|
tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, 0)
|
||||||
|
tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck, 0)
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, 0)
|
||||||
|
|
||||||
|
conn := tracker.connections[key]
|
||||||
|
require.Equal(t, TCPStateTimeWait, conn.GetState())
|
||||||
|
|
||||||
|
// Inbound SYN on same ports during TIME-WAIT: IsValidInbound returns false
|
||||||
|
// so the filter falls through to ACL check + TrackInbound (which creates
|
||||||
|
// a new connection via track() → updateIfExists skips TIME-WAIT for SYN)
|
||||||
|
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn, 0)
|
||||||
|
require.False(t, valid, "inbound SYN during TIME-WAIT should fail conntrack validation")
|
||||||
|
|
||||||
|
// Simulate what the filter does next: TrackInbound via the normal path
|
||||||
|
tracker.TrackInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn, nil, 100, 0)
|
||||||
|
|
||||||
|
// The new inbound connection uses the inverted key (dst→src becomes src→dst in track)
|
||||||
|
invertedKey := ConnKey{SrcIP: dstIP, DstIP: srcIP, SrcPort: dstPort, DstPort: srcPort}
|
||||||
|
newConn := tracker.connections[invertedKey]
|
||||||
|
require.NotNil(t, newConn, "new inbound connection should be tracked")
|
||||||
|
require.Equal(t, TCPStateSynReceived, newConn.GetState())
|
||||||
|
require.False(t, newConn.IsTombstone())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Late retransmit during TIME-WAIT still allowed", func(t *testing.T) {
|
||||||
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger)
|
||||||
|
defer tracker.Close()
|
||||||
|
|
||||||
|
key := ConnKey{SrcIP: srcIP, DstIP: dstIP, SrcPort: srcPort, DstPort: dstPort}
|
||||||
|
|
||||||
|
// Establish and active close → TIME-WAIT
|
||||||
|
establishConnection(t, tracker, srcIP, dstIP, srcPort, dstPort)
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck, 0)
|
||||||
|
tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, 0)
|
||||||
|
tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck, 0)
|
||||||
|
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, 0)
|
||||||
|
|
||||||
|
conn := tracker.connections[key]
|
||||||
|
require.Equal(t, TCPStateTimeWait, conn.GetState())
|
||||||
|
|
||||||
|
// Late ACK retransmits during TIME-WAIT should still be accepted
|
||||||
|
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, 0)
|
||||||
|
require.True(t, valid, "retransmitted ACK during TIME-WAIT should be accepted")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestTCPTimeoutHandling(t *testing.T) {
|
func TestTCPTimeoutHandling(t *testing.T) {
|
||||||
// Create tracker with a very short timeout for testing
|
// Create tracker with a very short timeout for testing
|
||||||
shortTimeout := 100 * time.Millisecond
|
shortTimeout := 100 * time.Millisecond
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,9 +18,18 @@ const (
|
|||||||
maxBatchSize = 1024 * 16
|
maxBatchSize = 1024 * 16
|
||||||
maxMessageSize = 1024 * 2
|
maxMessageSize = 1024 * 2
|
||||||
defaultFlushInterval = 2 * time.Second
|
defaultFlushInterval = 2 * time.Second
|
||||||
logChannelSize = 1000
|
defaultLogChanSize = 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func getLogChannelSize() int {
|
||||||
|
if v := os.Getenv("NB_USPFILTER_LOG_BUFFER"); v != "" {
|
||||||
|
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultLogChanSize
|
||||||
|
}
|
||||||
|
|
||||||
type Level uint32
|
type Level uint32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -69,7 +80,7 @@ type Logger struct {
|
|||||||
func NewFromLogrus(logrusLogger *log.Logger) *Logger {
|
func NewFromLogrus(logrusLogger *log.Logger) *Logger {
|
||||||
l := &Logger{
|
l := &Logger{
|
||||||
output: logrusLogger.Out,
|
output: logrusLogger.Out,
|
||||||
msgChannel: make(chan logMessage, logChannelSize),
|
msgChannel: make(chan logMessage, getLogChannelSize()),
|
||||||
shutdown: make(chan struct{}),
|
shutdown: make(chan struct{}),
|
||||||
bufPool: sync.Pool{
|
bufPool: sync.Pool{
|
||||||
New: func() any {
|
New: func() any {
|
||||||
|
|||||||
@@ -358,9 +358,9 @@ func incrementalUpdate(oldChecksum uint16, oldBytes, newBytes []byte) uint16 {
|
|||||||
// Fast path for IPv4 addresses (4 bytes) - most common case
|
// Fast path for IPv4 addresses (4 bytes) - most common case
|
||||||
if len(oldBytes) == 4 && len(newBytes) == 4 {
|
if len(oldBytes) == 4 && len(newBytes) == 4 {
|
||||||
sum += uint32(^binary.BigEndian.Uint16(oldBytes[0:2]))
|
sum += uint32(^binary.BigEndian.Uint16(oldBytes[0:2]))
|
||||||
sum += uint32(^binary.BigEndian.Uint16(oldBytes[2:4]))
|
sum += uint32(^binary.BigEndian.Uint16(oldBytes[2:4])) //nolint:gosec // length checked above
|
||||||
sum += uint32(binary.BigEndian.Uint16(newBytes[0:2]))
|
sum += uint32(binary.BigEndian.Uint16(newBytes[0:2]))
|
||||||
sum += uint32(binary.BigEndian.Uint16(newBytes[2:4]))
|
sum += uint32(binary.BigEndian.Uint16(newBytes[2:4])) //nolint:gosec // length checked above
|
||||||
} else {
|
} else {
|
||||||
// Fallback for other lengths
|
// Fallback for other lengths
|
||||||
for i := 0; i < len(oldBytes)-1; i += 2 {
|
for i := 0; i < len(oldBytes)-1; i += 2 {
|
||||||
|
|||||||
@@ -5,20 +5,18 @@ package configurer
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.zx2c4.com/wireguard/ipc"
|
"golang.zx2c4.com/wireguard/ipc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func openUAPI(deviceName string) (net.Listener, error) {
|
func openUAPI(deviceName string) (net.Listener, error) {
|
||||||
uapiSock, err := ipc.UAPIOpen(deviceName)
|
uapiSock, err := ipc.UAPIOpen(deviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to open uapi socket: %v", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
listener, err := ipc.UAPIListen(deviceName, uapiSock)
|
listener, err := ipc.UAPIListen(deviceName, uapiSock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to listen on uapi socket: %v", err)
|
_ = uapiSock.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ func NewUSPConfigurer(device *device.Device, deviceName string, activityRecorder
|
|||||||
return wgCfg
|
return wgCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewUSPConfigurerNoUAPI(device *device.Device, deviceName string, activityRecorder *bind.ActivityRecorder) *WGUSPConfigurer {
|
||||||
|
return &WGUSPConfigurer{
|
||||||
|
device: device,
|
||||||
|
deviceName: deviceName,
|
||||||
|
activityRecorder: activityRecorder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *WGUSPConfigurer) ConfigureInterface(privateKey string, port int) error {
|
func (c *WGUSPConfigurer) ConfigureInterface(privateKey string, port int) error {
|
||||||
log.Debugf("adding Wireguard private key")
|
log.Debugf("adding Wireguard private key")
|
||||||
key, err := wgtypes.ParseKey(privateKey)
|
key, err := wgtypes.ParseKey(privateKey)
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func (t *TunNetstackDevice) create() (WGConfigurer, error) {
|
|||||||
device.NewLogger(wgLogLevel(), "[netbird] "),
|
device.NewLogger(wgLogLevel(), "[netbird] "),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.configurer = configurer.NewUSPConfigurer(t.device, t.name, t.bind.ActivityRecorder())
|
t.configurer = configurer.NewUSPConfigurerNoUAPI(t.device, t.name, t.bind.ActivityRecorder())
|
||||||
err = t.configurer.ConfigureInterface(t.key, t.port)
|
err = t.configurer.ConfigureInterface(t.key, t.port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if cErr := tunIface.Close(); cErr != nil {
|
if cErr := tunIface.Close(); cErr != nil {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultMTU = 1280
|
DefaultMTU = 1420
|
||||||
MinMTU = 576
|
MinMTU = 576
|
||||||
MaxMTU = 8192
|
MaxMTU = 8192
|
||||||
DefaultWgPort = 51820
|
DefaultWgPort = 51820
|
||||||
|
|||||||
@@ -331,8 +331,11 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
state.Set(StatusConnected)
|
state.Set(StatusConnected)
|
||||||
|
|
||||||
if runningChan != nil {
|
if runningChan != nil {
|
||||||
close(runningChan)
|
select {
|
||||||
runningChan = nil
|
case <-runningChan:
|
||||||
|
default:
|
||||||
|
close(runningChan)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<-engineCtx.Done()
|
<-engineCtx.Done()
|
||||||
|
|||||||
60
client/internal/daemonaddr/resolve.go
Normal file
60
client/internal/daemonaddr/resolve.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
//go:build !windows && !ios && !android
|
||||||
|
|
||||||
|
package daemonaddr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var scanDir = "/var/run/netbird"
|
||||||
|
|
||||||
|
// setScanDir overrides the scan directory (used by tests).
|
||||||
|
func setScanDir(dir string) {
|
||||||
|
scanDir = dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveUnixDaemonAddr checks whether the default Unix socket exists and, if not,
|
||||||
|
// scans /var/run/netbird/ for a single .sock file to use instead. This handles the
|
||||||
|
// mismatch between the netbird@.service template (which places the socket under
|
||||||
|
// /var/run/netbird/<instance>.sock) and the CLI default (/var/run/netbird.sock).
|
||||||
|
func ResolveUnixDaemonAddr(addr string) string {
|
||||||
|
if !strings.HasPrefix(addr, "unix://") {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
sockPath := strings.TrimPrefix(addr, "unix://")
|
||||||
|
if _, err := os.Stat(sockPath); err == nil {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(scanDir)
|
||||||
|
if err != nil {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
var found []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(e.Name(), ".sock") {
|
||||||
|
found = append(found, filepath.Join(scanDir, e.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(found) {
|
||||||
|
case 1:
|
||||||
|
resolved := "unix://" + found[0]
|
||||||
|
log.Debugf("Default daemon socket not found, using discovered socket: %s", resolved)
|
||||||
|
return resolved
|
||||||
|
case 0:
|
||||||
|
return addr
|
||||||
|
default:
|
||||||
|
log.Warnf("Default daemon socket not found and multiple sockets discovered in %s; pass --daemon-addr explicitly", scanDir)
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
}
|
||||||
8
client/internal/daemonaddr/resolve_stub.go
Normal file
8
client/internal/daemonaddr/resolve_stub.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build windows || ios || android
|
||||||
|
|
||||||
|
package daemonaddr
|
||||||
|
|
||||||
|
// ResolveUnixDaemonAddr is a no-op on platforms that don't use Unix sockets.
|
||||||
|
func ResolveUnixDaemonAddr(addr string) string {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
121
client/internal/daemonaddr/resolve_test.go
Normal file
121
client/internal/daemonaddr/resolve_test.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
//go:build !windows && !ios && !android
|
||||||
|
|
||||||
|
package daemonaddr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createSockFile creates a regular file with a .sock extension.
|
||||||
|
// ResolveUnixDaemonAddr uses os.Stat (not net.Dial), so a regular file is
|
||||||
|
// sufficient and avoids Unix socket path-length limits on macOS.
|
||||||
|
func createSockFile(t *testing.T, path string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.WriteFile(path, nil, 0o600); err != nil {
|
||||||
|
t.Fatalf("failed to create test sock file at %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveUnixDaemonAddr_DefaultExists(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
sock := filepath.Join(tmp, "netbird.sock")
|
||||||
|
createSockFile(t, sock)
|
||||||
|
|
||||||
|
addr := "unix://" + sock
|
||||||
|
got := ResolveUnixDaemonAddr(addr)
|
||||||
|
if got != addr {
|
||||||
|
t.Errorf("expected %s, got %s", addr, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveUnixDaemonAddr_SingleDiscovered(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
|
||||||
|
// Default socket does not exist
|
||||||
|
defaultAddr := "unix://" + filepath.Join(tmp, "netbird.sock")
|
||||||
|
|
||||||
|
// Create a scan dir with one socket
|
||||||
|
sd := filepath.Join(tmp, "netbird")
|
||||||
|
if err := os.MkdirAll(sd, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
instanceSock := filepath.Join(sd, "main.sock")
|
||||||
|
createSockFile(t, instanceSock)
|
||||||
|
|
||||||
|
origScanDir := scanDir
|
||||||
|
setScanDir(sd)
|
||||||
|
t.Cleanup(func() { setScanDir(origScanDir) })
|
||||||
|
|
||||||
|
got := ResolveUnixDaemonAddr(defaultAddr)
|
||||||
|
expected := "unix://" + instanceSock
|
||||||
|
if got != expected {
|
||||||
|
t.Errorf("expected %s, got %s", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveUnixDaemonAddr_MultipleDiscovered(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
|
||||||
|
defaultAddr := "unix://" + filepath.Join(tmp, "netbird.sock")
|
||||||
|
|
||||||
|
sd := filepath.Join(tmp, "netbird")
|
||||||
|
if err := os.MkdirAll(sd, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
createSockFile(t, filepath.Join(sd, "main.sock"))
|
||||||
|
createSockFile(t, filepath.Join(sd, "other.sock"))
|
||||||
|
|
||||||
|
origScanDir := scanDir
|
||||||
|
setScanDir(sd)
|
||||||
|
t.Cleanup(func() { setScanDir(origScanDir) })
|
||||||
|
|
||||||
|
got := ResolveUnixDaemonAddr(defaultAddr)
|
||||||
|
if got != defaultAddr {
|
||||||
|
t.Errorf("expected original %s, got %s", defaultAddr, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveUnixDaemonAddr_NoSocketsFound(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
|
||||||
|
defaultAddr := "unix://" + filepath.Join(tmp, "netbird.sock")
|
||||||
|
|
||||||
|
sd := filepath.Join(tmp, "netbird")
|
||||||
|
if err := os.MkdirAll(sd, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
origScanDir := scanDir
|
||||||
|
setScanDir(sd)
|
||||||
|
t.Cleanup(func() { setScanDir(origScanDir) })
|
||||||
|
|
||||||
|
got := ResolveUnixDaemonAddr(defaultAddr)
|
||||||
|
if got != defaultAddr {
|
||||||
|
t.Errorf("expected original %s, got %s", defaultAddr, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveUnixDaemonAddr_NonUnixAddr(t *testing.T) {
|
||||||
|
addr := "tcp://127.0.0.1:41731"
|
||||||
|
got := ResolveUnixDaemonAddr(addr)
|
||||||
|
if got != addr {
|
||||||
|
t.Errorf("expected %s, got %s", addr, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveUnixDaemonAddr_ScanDirMissing(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
|
||||||
|
defaultAddr := "unix://" + filepath.Join(tmp, "netbird.sock")
|
||||||
|
|
||||||
|
origScanDir := scanDir
|
||||||
|
setScanDir(filepath.Join(tmp, "nonexistent"))
|
||||||
|
t.Cleanup(func() { setScanDir(origScanDir) })
|
||||||
|
|
||||||
|
got := ResolveUnixDaemonAddr(defaultAddr)
|
||||||
|
if got != defaultAddr {
|
||||||
|
t.Errorf("expected original %s, got %s", defaultAddr, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
netbirdDNSStateKeyFormat = "State:/Network/Service/NetBird-%s/DNS"
|
netbirdDNSStateKeyFormat = "State:/Network/Service/NetBird-%s/DNS"
|
||||||
|
netbirdDNSStateKeyIndexedFormat = "State:/Network/Service/NetBird-%s-%d/DNS"
|
||||||
globalIPv4State = "State:/Network/Global/IPv4"
|
globalIPv4State = "State:/Network/Global/IPv4"
|
||||||
primaryServiceStateKeyFormat = "State:/Network/Service/%s/DNS"
|
primaryServiceStateKeyFormat = "State:/Network/Service/%s/DNS"
|
||||||
keySupplementalMatchDomains = "SupplementalMatchDomains"
|
keySupplementalMatchDomains = "SupplementalMatchDomains"
|
||||||
@@ -35,6 +38,14 @@ const (
|
|||||||
searchSuffix = "Search"
|
searchSuffix = "Search"
|
||||||
matchSuffix = "Match"
|
matchSuffix = "Match"
|
||||||
localSuffix = "Local"
|
localSuffix = "Local"
|
||||||
|
|
||||||
|
// maxDomainsPerResolverEntry is the max number of domains per scutil resolver key.
|
||||||
|
// scutil's d.add has maxArgs=101 (key + * + 99 values), so 99 is the hard cap.
|
||||||
|
maxDomainsPerResolverEntry = 50
|
||||||
|
|
||||||
|
// maxDomainBytesPerResolverEntry is the max total bytes of domain strings per key.
|
||||||
|
// scutil has an undocumented ~2048 byte value buffer; we stay well under it.
|
||||||
|
maxDomainBytesPerResolverEntry = 1500
|
||||||
)
|
)
|
||||||
|
|
||||||
type systemConfigurator struct {
|
type systemConfigurator struct {
|
||||||
@@ -84,28 +95,23 @@ func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *
|
|||||||
searchDomains = append(searchDomains, strings.TrimSuffix(""+dConf.Domain, "."))
|
searchDomains = append(searchDomains, strings.TrimSuffix(""+dConf.Domain, "."))
|
||||||
}
|
}
|
||||||
|
|
||||||
matchKey := getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)
|
if err := s.removeKeysContaining(matchSuffix); err != nil {
|
||||||
var err error
|
log.Warnf("failed to remove old match keys: %v", err)
|
||||||
if len(matchDomains) != 0 {
|
|
||||||
err = s.addMatchDomains(matchKey, strings.Join(matchDomains, " "), config.ServerIP, config.ServerPort)
|
|
||||||
} else {
|
|
||||||
log.Infof("removing match domains from the system")
|
|
||||||
err = s.removeKeyFromSystemConfig(matchKey)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if len(matchDomains) != 0 {
|
||||||
return fmt.Errorf("add match domains: %w", err)
|
if err := s.addBatchedDomains(matchSuffix, matchDomains, config.ServerIP, config.ServerPort, false); err != nil {
|
||||||
|
return fmt.Errorf("add match domains: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
s.updateState(stateManager)
|
s.updateState(stateManager)
|
||||||
|
|
||||||
searchKey := getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix)
|
if err := s.removeKeysContaining(searchSuffix); err != nil {
|
||||||
if len(searchDomains) != 0 {
|
log.Warnf("failed to remove old search keys: %v", err)
|
||||||
err = s.addSearchDomains(searchKey, strings.Join(searchDomains, " "), config.ServerIP, config.ServerPort)
|
|
||||||
} else {
|
|
||||||
log.Infof("removing search domains from the system")
|
|
||||||
err = s.removeKeyFromSystemConfig(searchKey)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if len(searchDomains) != 0 {
|
||||||
return fmt.Errorf("add search domains: %w", err)
|
if err := s.addBatchedDomains(searchSuffix, searchDomains, config.ServerIP, config.ServerPort, true); err != nil {
|
||||||
|
return fmt.Errorf("add search domains: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
s.updateState(stateManager)
|
s.updateState(stateManager)
|
||||||
|
|
||||||
@@ -149,8 +155,7 @@ func (s *systemConfigurator) restoreHostDNS() error {
|
|||||||
|
|
||||||
func (s *systemConfigurator) getRemovableKeysWithDefaults() []string {
|
func (s *systemConfigurator) getRemovableKeysWithDefaults() []string {
|
||||||
if len(s.createdKeys) == 0 {
|
if len(s.createdKeys) == 0 {
|
||||||
// return defaults for startup calls
|
return s.discoverExistingKeys()
|
||||||
return []string{getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix), getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
keys := make([]string, 0, len(s.createdKeys))
|
keys := make([]string, 0, len(s.createdKeys))
|
||||||
@@ -160,6 +165,47 @@ func (s *systemConfigurator) getRemovableKeysWithDefaults() []string {
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// discoverExistingKeys probes scutil for all NetBird DNS keys that may exist.
|
||||||
|
// This handles the case where createdKeys is empty (e.g., state file lost after unclean shutdown).
|
||||||
|
func (s *systemConfigurator) discoverExistingKeys() []string {
|
||||||
|
dnsKeys, err := getSystemDNSKeys()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to get system DNS keys: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys []string
|
||||||
|
|
||||||
|
for _, suffix := range []string{searchSuffix, matchSuffix, localSuffix} {
|
||||||
|
key := getKeyWithInput(netbirdDNSStateKeyFormat, suffix)
|
||||||
|
if strings.Contains(dnsKeys, key) {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, suffix := range []string{searchSuffix, matchSuffix} {
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
key := fmt.Sprintf(netbirdDNSStateKeyIndexedFormat, suffix, i)
|
||||||
|
if !strings.Contains(dnsKeys, key) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSystemDNSKeys gets all DNS keys
|
||||||
|
func getSystemDNSKeys() (string, error) {
|
||||||
|
command := "list .*DNS\nquit\n"
|
||||||
|
out, err := runSystemConfigCommand(command)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error {
|
func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error {
|
||||||
line := buildRemoveKeyOperation(key)
|
line := buildRemoveKeyOperation(key)
|
||||||
_, err := runSystemConfigCommand(wrapCommand(line))
|
_, err := runSystemConfigCommand(wrapCommand(line))
|
||||||
@@ -184,12 +230,11 @@ func (s *systemConfigurator) addLocalDNS() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.addSearchDomains(
|
domainsStr := strings.Join(s.systemDNSSettings.Domains, " ")
|
||||||
localKey,
|
if err := s.addDNSState(localKey, domainsStr, s.systemDNSSettings.ServerIP, s.systemDNSSettings.ServerPort, true); err != nil {
|
||||||
strings.Join(s.systemDNSSettings.Domains, " "), s.systemDNSSettings.ServerIP, s.systemDNSSettings.ServerPort,
|
return fmt.Errorf("add local dns state: %w", err)
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("add search domains: %w", err)
|
|
||||||
}
|
}
|
||||||
|
s.createdKeys[localKey] = struct{}{}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -280,28 +325,77 @@ func (s *systemConfigurator) getOriginalNameservers() []netip.Addr {
|
|||||||
return slices.Clone(s.origNameservers)
|
return slices.Clone(s.origNameservers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemConfigurator) addSearchDomains(key, domains string, ip netip.Addr, port int) error {
|
// splitDomainsIntoBatches splits domains into batches respecting both element count and byte size limits.
|
||||||
err := s.addDNSState(key, domains, ip, port, true)
|
func splitDomainsIntoBatches(domains []string) [][]string {
|
||||||
if err != nil {
|
if len(domains) == 0 {
|
||||||
return fmt.Errorf("add dns state: %w", err)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("added %d search domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains)
|
var batches [][]string
|
||||||
|
var current []string
|
||||||
|
currentBytes := 0
|
||||||
|
|
||||||
s.createdKeys[key] = struct{}{}
|
for _, d := range domains {
|
||||||
|
domainLen := len(d)
|
||||||
|
newBytes := currentBytes + domainLen
|
||||||
|
if currentBytes > 0 {
|
||||||
|
newBytes++ // space separator
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
if len(current) > 0 && (len(current) >= maxDomainsPerResolverEntry || newBytes > maxDomainBytesPerResolverEntry) {
|
||||||
|
batches = append(batches, current)
|
||||||
|
current = nil
|
||||||
|
currentBytes = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
current = append(current, d)
|
||||||
|
if currentBytes > 0 {
|
||||||
|
currentBytes += 1 + domainLen
|
||||||
|
} else {
|
||||||
|
currentBytes = domainLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(current) > 0 {
|
||||||
|
batches = append(batches, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return batches
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemConfigurator) addMatchDomains(key, domains string, dnsServer netip.Addr, port int) error {
|
// removeKeysContaining removes all created keys that contain the given substring.
|
||||||
err := s.addDNSState(key, domains, dnsServer, port, false)
|
func (s *systemConfigurator) removeKeysContaining(suffix string) error {
|
||||||
if err != nil {
|
var toRemove []string
|
||||||
return fmt.Errorf("add dns state: %w", err)
|
for key := range s.createdKeys {
|
||||||
|
if strings.Contains(key, suffix) {
|
||||||
|
toRemove = append(toRemove, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var multiErr *multierror.Error
|
||||||
|
for _, key := range toRemove {
|
||||||
|
if err := s.removeKeyFromSystemConfig(key); err != nil {
|
||||||
|
multiErr = multierror.Append(multiErr, fmt.Errorf("couldn't remove key %s: %w", key, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nberrors.FormatErrorOrNil(multiErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addBatchedDomains splits domains into batches and creates indexed scutil keys for each batch.
|
||||||
|
func (s *systemConfigurator) addBatchedDomains(suffix string, domains []string, ip netip.Addr, port int, enableSearch bool) error {
|
||||||
|
batches := splitDomainsIntoBatches(domains)
|
||||||
|
|
||||||
|
for i, batch := range batches {
|
||||||
|
key := fmt.Sprintf(netbirdDNSStateKeyIndexedFormat, suffix, i)
|
||||||
|
domainsStr := strings.Join(batch, " ")
|
||||||
|
|
||||||
|
if err := s.addDNSState(key, domainsStr, ip, port, enableSearch); err != nil {
|
||||||
|
return fmt.Errorf("add dns state for batch %d: %w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.createdKeys[key] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("added %d match domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains)
|
log.Infof("added %d %s domains across %d resolver entries", len(domains), suffix, len(batches))
|
||||||
|
|
||||||
s.createdKeys[key] = struct{}{}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -364,7 +458,6 @@ func (s *systemConfigurator) flushDNSCache() error {
|
|||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
return fmt.Errorf("restart mDNSResponder: %w, output: %s", err, out)
|
return fmt.Errorf("restart mDNSResponder: %w, output: %s", err, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("flushed DNS cache")
|
log.Info("flushed DNS cache")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -49,17 +52,22 @@ func TestDarwinDNSUncleanShutdownCleanup(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, sm.PersistState(context.Background()))
|
require.NoError(t, sm.PersistState(context.Background()))
|
||||||
|
|
||||||
searchKey := getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix)
|
|
||||||
matchKey := getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)
|
|
||||||
localKey := getKeyWithInput(netbirdDNSStateKeyFormat, localSuffix)
|
localKey := getKeyWithInput(netbirdDNSStateKeyFormat, localSuffix)
|
||||||
|
|
||||||
|
// Collect all created keys for cleanup verification
|
||||||
|
createdKeys := make([]string, 0, len(configurator.createdKeys))
|
||||||
|
for key := range configurator.createdKeys {
|
||||||
|
createdKeys = append(createdKeys, key)
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
for _, key := range []string{searchKey, matchKey, localKey} {
|
for _, key := range createdKeys {
|
||||||
_ = removeTestDNSKey(key)
|
_ = removeTestDNSKey(key)
|
||||||
}
|
}
|
||||||
|
_ = removeTestDNSKey(localKey)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, key := range []string{searchKey, matchKey, localKey} {
|
for _, key := range createdKeys {
|
||||||
exists, err := checkDNSKeyExists(key)
|
exists, err := checkDNSKeyExists(key)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if exists {
|
if exists {
|
||||||
@@ -83,13 +91,223 @@ func TestDarwinDNSUncleanShutdownCleanup(t *testing.T) {
|
|||||||
err = shutdownState.Cleanup()
|
err = shutdownState.Cleanup()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, key := range []string{searchKey, matchKey, localKey} {
|
for _, key := range createdKeys {
|
||||||
exists, err := checkDNSKeyExists(key)
|
exists, err := checkDNSKeyExists(key)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, exists, "Key %s should NOT exist after cleanup", key)
|
assert.False(t, exists, "Key %s should NOT exist after cleanup", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateShortDomains generates domains like a.com, b.com, ..., aa.com, ab.com, etc.
|
||||||
|
func generateShortDomains(count int) []string {
|
||||||
|
domains := make([]string, 0, count)
|
||||||
|
for i := range count {
|
||||||
|
label := ""
|
||||||
|
n := i
|
||||||
|
for {
|
||||||
|
label = string(rune('a'+n%26)) + label
|
||||||
|
n = n/26 - 1
|
||||||
|
if n < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
domains = append(domains, label+".com")
|
||||||
|
}
|
||||||
|
return domains
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateLongDomains generates domains like subdomain-000.department.organization-name.example.com
|
||||||
|
func generateLongDomains(count int) []string {
|
||||||
|
domains := make([]string, 0, count)
|
||||||
|
for i := range count {
|
||||||
|
domains = append(domains, fmt.Sprintf("subdomain-%03d.department.organization-name.example.com", i))
|
||||||
|
}
|
||||||
|
return domains
|
||||||
|
}
|
||||||
|
|
||||||
|
// readDomainsFromKey reads the SupplementalMatchDomains array back from scutil for a given key.
|
||||||
|
func readDomainsFromKey(t *testing.T, key string) []string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cmd := exec.Command(scutilPath)
|
||||||
|
cmd.Stdin = strings.NewReader(fmt.Sprintf("open\nshow %s\nquit\n", key))
|
||||||
|
out, err := cmd.Output()
|
||||||
|
require.NoError(t, err, "scutil show should succeed")
|
||||||
|
|
||||||
|
var domains []string
|
||||||
|
inArray := false
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(out))
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if strings.HasPrefix(line, "SupplementalMatchDomains") && strings.Contains(line, "<array>") {
|
||||||
|
inArray = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inArray {
|
||||||
|
if line == "}" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// lines look like: "0 : a.com"
|
||||||
|
parts := strings.SplitN(line, " : ", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
domains = append(domains, parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NoError(t, scanner.Err())
|
||||||
|
return domains
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitDomainsIntoBatches(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
domains []string
|
||||||
|
expectedCount int
|
||||||
|
checkAllPresent bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
domains: nil,
|
||||||
|
expectedCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "under_limit",
|
||||||
|
domains: generateShortDomains(10),
|
||||||
|
expectedCount: 1,
|
||||||
|
checkAllPresent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "at_element_limit",
|
||||||
|
domains: generateShortDomains(50),
|
||||||
|
expectedCount: 1,
|
||||||
|
checkAllPresent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "over_element_limit",
|
||||||
|
domains: generateShortDomains(51),
|
||||||
|
expectedCount: 2,
|
||||||
|
checkAllPresent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "triple_element_limit",
|
||||||
|
domains: generateShortDomains(150),
|
||||||
|
expectedCount: 3,
|
||||||
|
checkAllPresent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "long_domains_hit_byte_limit",
|
||||||
|
domains: generateLongDomains(50),
|
||||||
|
checkAllPresent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "500_short_domains",
|
||||||
|
domains: generateShortDomains(500),
|
||||||
|
expectedCount: 10,
|
||||||
|
checkAllPresent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "500_long_domains",
|
||||||
|
domains: generateLongDomains(500),
|
||||||
|
checkAllPresent: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
batches := splitDomainsIntoBatches(tc.domains)
|
||||||
|
|
||||||
|
if tc.expectedCount > 0 {
|
||||||
|
assert.Len(t, batches, tc.expectedCount, "expected %d batches", tc.expectedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify each batch respects limits
|
||||||
|
for i, batch := range batches {
|
||||||
|
assert.LessOrEqual(t, len(batch), maxDomainsPerResolverEntry,
|
||||||
|
"batch %d exceeds element limit", i)
|
||||||
|
|
||||||
|
totalBytes := 0
|
||||||
|
for j, d := range batch {
|
||||||
|
if j > 0 {
|
||||||
|
totalBytes++
|
||||||
|
}
|
||||||
|
totalBytes += len(d)
|
||||||
|
}
|
||||||
|
assert.LessOrEqual(t, totalBytes, maxDomainBytesPerResolverEntry,
|
||||||
|
"batch %d exceeds byte limit (%d bytes)", i, totalBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.checkAllPresent {
|
||||||
|
var all []string
|
||||||
|
for _, batch := range batches {
|
||||||
|
all = append(all, batch...)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.domains, all, "all domains should be present in order")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMatchDomainBatching writes increasing numbers of domains via the batching mechanism
|
||||||
|
// and verifies all domains are readable across multiple scutil keys.
|
||||||
|
func TestMatchDomainBatching(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping scutil integration test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
count int
|
||||||
|
generator func(int) []string
|
||||||
|
}{
|
||||||
|
{"short_10", 10, generateShortDomains},
|
||||||
|
{"short_50", 50, generateShortDomains},
|
||||||
|
{"short_100", 100, generateShortDomains},
|
||||||
|
{"short_200", 200, generateShortDomains},
|
||||||
|
{"short_500", 500, generateShortDomains},
|
||||||
|
{"long_10", 10, generateLongDomains},
|
||||||
|
{"long_50", 50, generateLongDomains},
|
||||||
|
{"long_100", 100, generateLongDomains},
|
||||||
|
{"long_200", 200, generateLongDomains},
|
||||||
|
{"long_500", 500, generateLongDomains},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
configurator := &systemConfigurator{
|
||||||
|
createdKeys: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
for key := range configurator.createdKeys {
|
||||||
|
_ = removeTestDNSKey(key)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
domains := tc.generator(tc.count)
|
||||||
|
err := configurator.addBatchedDomains(matchSuffix, domains, netip.MustParseAddr("100.64.0.1"), 53, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
batches := splitDomainsIntoBatches(domains)
|
||||||
|
t.Logf("wrote %d domains across %d batched keys", tc.count, len(batches))
|
||||||
|
|
||||||
|
// Read back all domains from all batched keys
|
||||||
|
var got []string
|
||||||
|
for i := range batches {
|
||||||
|
key := fmt.Sprintf(netbirdDNSStateKeyIndexedFormat, matchSuffix, i)
|
||||||
|
exists, err := checkDNSKeyExists(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, exists, "key %s should exist", key)
|
||||||
|
|
||||||
|
got = append(got, readDomainsFromKey(t, key)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("read back %d/%d domains from %d keys", len(got), tc.count, len(batches))
|
||||||
|
assert.Equal(t, tc.count, len(got), "all domains should be readable")
|
||||||
|
assert.Equal(t, domains, got, "domains should match in order")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func checkDNSKeyExists(key string) (bool, error) {
|
func checkDNSKeyExists(key string) (bool, error) {
|
||||||
cmd := exec.Command(scutilPath)
|
cmd := exec.Command(scutilPath)
|
||||||
cmd.Stdin = strings.NewReader("show " + key + "\nquit\n")
|
cmd.Stdin = strings.NewReader("show " + key + "\nquit\n")
|
||||||
@@ -158,15 +376,15 @@ func setupTestConfigurator(t *testing.T) (*systemConfigurator, *statemanager.Man
|
|||||||
createdKeys: make(map[string]struct{}),
|
createdKeys: make(map[string]struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
searchKey := getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix)
|
|
||||||
matchKey := getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)
|
|
||||||
localKey := getKeyWithInput(netbirdDNSStateKeyFormat, localSuffix)
|
|
||||||
|
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
_ = sm.Stop(context.Background())
|
_ = sm.Stop(context.Background())
|
||||||
for _, key := range []string{searchKey, matchKey, localKey} {
|
for key := range configurator.createdKeys {
|
||||||
_ = removeTestDNSKey(key)
|
_ = removeTestDNSKey(key)
|
||||||
}
|
}
|
||||||
|
// Also clean up old-format keys and local key in case they exist
|
||||||
|
_ = removeTestDNSKey(getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix))
|
||||||
|
_ = removeTestDNSKey(getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix))
|
||||||
|
_ = removeTestDNSKey(getKeyWithInput(netbirdDNSStateKeyFormat, localSuffix))
|
||||||
}
|
}
|
||||||
|
|
||||||
return configurator, sm, cleanup
|
return configurator, sm, cleanup
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ const (
|
|||||||
dnsPolicyConfigConfigOptionsKey = "ConfigOptions"
|
dnsPolicyConfigConfigOptionsKey = "ConfigOptions"
|
||||||
dnsPolicyConfigConfigOptionsValue = 0x8
|
dnsPolicyConfigConfigOptionsValue = 0x8
|
||||||
|
|
||||||
|
nrptMaxDomainsPerRule = 50
|
||||||
|
|
||||||
interfaceConfigPath = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces`
|
interfaceConfigPath = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces`
|
||||||
interfaceConfigNameServerKey = "NameServer"
|
interfaceConfigNameServerKey = "NameServer"
|
||||||
interfaceConfigSearchListKey = "SearchList"
|
interfaceConfigSearchListKey = "SearchList"
|
||||||
@@ -198,10 +200,11 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager
|
|||||||
|
|
||||||
if len(matchDomains) != 0 {
|
if len(matchDomains) != 0 {
|
||||||
count, err := r.addDNSMatchPolicy(matchDomains, config.ServerIP)
|
count, err := r.addDNSMatchPolicy(matchDomains, config.ServerIP)
|
||||||
|
// Update count even on error to ensure cleanup covers partially created rules
|
||||||
|
r.nrptEntryCount = count
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add dns match policy: %w", err)
|
return fmt.Errorf("add dns match policy: %w", err)
|
||||||
}
|
}
|
||||||
r.nrptEntryCount = count
|
|
||||||
} else {
|
} else {
|
||||||
r.nrptEntryCount = 0
|
r.nrptEntryCount = 0
|
||||||
}
|
}
|
||||||
@@ -239,23 +242,33 @@ func (r *registryConfigurator) addDNSSetupForAll(ip netip.Addr) error {
|
|||||||
func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip netip.Addr) (int, error) {
|
func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip netip.Addr) (int, error) {
|
||||||
// if the gpo key is present, we need to put our DNS settings there, otherwise our config might be ignored
|
// if the gpo key is present, we need to put our DNS settings there, otherwise our config might be ignored
|
||||||
// see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnrpt/8cc31cb9-20cb-4140-9e85-3e08703b4745
|
// see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnrpt/8cc31cb9-20cb-4140-9e85-3e08703b4745
|
||||||
for i, domain := range domains {
|
|
||||||
localPath := fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i)
|
|
||||||
gpoPath := fmt.Sprintf("%s-%d", gpoDnsPolicyConfigMatchPath, i)
|
|
||||||
|
|
||||||
singleDomain := []string{domain}
|
// We need to batch domains into chunks and create one NRPT rule per batch.
|
||||||
|
ruleIndex := 0
|
||||||
|
for i := 0; i < len(domains); i += nrptMaxDomainsPerRule {
|
||||||
|
end := i + nrptMaxDomainsPerRule
|
||||||
|
if end > len(domains) {
|
||||||
|
end = len(domains)
|
||||||
|
}
|
||||||
|
batchDomains := domains[i:end]
|
||||||
|
|
||||||
if err := r.configureDNSPolicy(localPath, singleDomain, ip); err != nil {
|
localPath := fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, ruleIndex)
|
||||||
return i, fmt.Errorf("configure DNS Local policy for domain %s: %w", domain, err)
|
gpoPath := fmt.Sprintf("%s-%d", gpoDnsPolicyConfigMatchPath, ruleIndex)
|
||||||
|
|
||||||
|
if err := r.configureDNSPolicy(localPath, batchDomains, ip); err != nil {
|
||||||
|
return ruleIndex, fmt.Errorf("configure DNS Local policy for rule %d: %w", ruleIndex, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Increment immediately so the caller's cleanup path knows about this rule
|
||||||
|
ruleIndex++
|
||||||
|
|
||||||
if r.gpo {
|
if r.gpo {
|
||||||
if err := r.configureDNSPolicy(gpoPath, singleDomain, ip); err != nil {
|
if err := r.configureDNSPolicy(gpoPath, batchDomains, ip); err != nil {
|
||||||
return i, fmt.Errorf("configure gpo DNS policy: %w", err)
|
return ruleIndex, fmt.Errorf("configure gpo DNS policy for rule %d: %w", ruleIndex-1, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("added NRPT entry for domain: %s", domain)
|
log.Debugf("added NRPT rule %d with %d domains", ruleIndex-1, len(batchDomains))
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.gpo {
|
if r.gpo {
|
||||||
@@ -264,8 +277,8 @@ func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip netip.Addr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("added %d separate NRPT entries. Domain list: %s", len(domains), domains)
|
log.Infof("added %d NRPT rules for %d domains", ruleIndex, len(domains))
|
||||||
return len(domains), nil
|
return ruleIndex, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []string, ip netip.Addr) error {
|
func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []string, ip netip.Addr) error {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
// TestNRPTEntriesCleanupOnConfigChange tests that old NRPT entries are properly cleaned up
|
// TestNRPTEntriesCleanupOnConfigChange tests that old NRPT entries are properly cleaned up
|
||||||
// when the number of match domains decreases between configuration changes.
|
// when the number of match domains decreases between configuration changes.
|
||||||
|
// With batching enabled (50 domains per rule), we need enough domains to create multiple rules.
|
||||||
func TestNRPTEntriesCleanupOnConfigChange(t *testing.T) {
|
func TestNRPTEntriesCleanupOnConfigChange(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("skipping registry integration test in short mode")
|
t.Skip("skipping registry integration test in short mode")
|
||||||
@@ -37,51 +38,60 @@ func TestNRPTEntriesCleanupOnConfigChange(t *testing.T) {
|
|||||||
gpo: false,
|
gpo: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
config5 := HostDNSConfig{
|
// Create 125 domains which will result in 3 NRPT rules (50+50+25)
|
||||||
ServerIP: testIP,
|
domains125 := make([]DomainConfig, 125)
|
||||||
Domains: []DomainConfig{
|
for i := 0; i < 125; i++ {
|
||||||
{Domain: "domain1.com", MatchOnly: true},
|
domains125[i] = DomainConfig{
|
||||||
{Domain: "domain2.com", MatchOnly: true},
|
Domain: fmt.Sprintf("domain%d.com", i+1),
|
||||||
{Domain: "domain3.com", MatchOnly: true},
|
MatchOnly: true,
|
||||||
{Domain: "domain4.com", MatchOnly: true},
|
}
|
||||||
{Domain: "domain5.com", MatchOnly: true},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cfg.applyDNSConfig(config5, nil)
|
config125 := HostDNSConfig{
|
||||||
|
ServerIP: testIP,
|
||||||
|
Domains: domains125,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cfg.applyDNSConfig(config125, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify all 5 entries exist
|
// Verify 3 NRPT rules exist
|
||||||
for i := 0; i < 5; i++ {
|
assert.Equal(t, 3, cfg.nrptEntryCount, "Should create 3 NRPT rules for 125 domains")
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i))
|
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, exists, "Entry %d should exist after first config", i)
|
assert.True(t, exists, "NRPT rule %d should exist after first config", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
config2 := HostDNSConfig{
|
// Reduce to 75 domains which will result in 2 NRPT rules (50+25)
|
||||||
|
domains75 := make([]DomainConfig, 75)
|
||||||
|
for i := 0; i < 75; i++ {
|
||||||
|
domains75[i] = DomainConfig{
|
||||||
|
Domain: fmt.Sprintf("domain%d.com", i+1),
|
||||||
|
MatchOnly: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config75 := HostDNSConfig{
|
||||||
ServerIP: testIP,
|
ServerIP: testIP,
|
||||||
Domains: []DomainConfig{
|
Domains: domains75,
|
||||||
{Domain: "domain1.com", MatchOnly: true},
|
|
||||||
{Domain: "domain2.com", MatchOnly: true},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cfg.applyDNSConfig(config2, nil)
|
err = cfg.applyDNSConfig(config75, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify first 2 entries exist
|
// Verify first 2 NRPT rules exist
|
||||||
|
assert.Equal(t, 2, cfg.nrptEntryCount, "Should create 2 NRPT rules for 75 domains")
|
||||||
for i := 0; i < 2; i++ {
|
for i := 0; i < 2; i++ {
|
||||||
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i))
|
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, exists, "Entry %d should exist after second config", i)
|
assert.True(t, exists, "NRPT rule %d should exist after second config", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify entries 2-4 are cleaned up
|
// Verify rule 2 is cleaned up
|
||||||
for i := 2; i < 5; i++ {
|
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, 2))
|
||||||
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i))
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
assert.False(t, exists, "NRPT rule 2 should NOT exist after reducing to 75 domains")
|
||||||
assert.False(t, exists, "Entry %d should NOT exist after reducing to 2 domains", i)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func registryKeyExists(path string) (bool, error) {
|
func registryKeyExists(path string) (bool, error) {
|
||||||
@@ -97,6 +107,106 @@ func registryKeyExists(path string) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cleanupRegistryKeys(*testing.T) {
|
func cleanupRegistryKeys(*testing.T) {
|
||||||
cfg := ®istryConfigurator{nrptEntryCount: 10}
|
// Clean up more entries to account for batching tests with many domains
|
||||||
|
cfg := ®istryConfigurator{nrptEntryCount: 20}
|
||||||
_ = cfg.removeDNSMatchPolicies()
|
_ = cfg.removeDNSMatchPolicies()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNRPTDomainBatching verifies that domains are correctly batched into NRPT rules.
|
||||||
|
func TestNRPTDomainBatching(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping registry integration test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer cleanupRegistryKeys(t)
|
||||||
|
cleanupRegistryKeys(t)
|
||||||
|
|
||||||
|
testIP := netip.MustParseAddr("100.64.0.1")
|
||||||
|
|
||||||
|
// Create a test interface registry key so updateSearchDomains doesn't fail
|
||||||
|
testGUID := "{12345678-1234-1234-1234-123456789ABC}"
|
||||||
|
interfacePath := `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\` + testGUID
|
||||||
|
testKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, interfacePath, registry.SET_VALUE)
|
||||||
|
require.NoError(t, err, "Should create test interface registry key")
|
||||||
|
testKey.Close()
|
||||||
|
defer func() {
|
||||||
|
_ = registry.DeleteKey(registry.LOCAL_MACHINE, interfacePath)
|
||||||
|
}()
|
||||||
|
|
||||||
|
cfg := ®istryConfigurator{
|
||||||
|
guid: testGUID,
|
||||||
|
gpo: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
domainCount int
|
||||||
|
expectedRuleCount int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Less than 50 domains (single rule)",
|
||||||
|
domainCount: 30,
|
||||||
|
expectedRuleCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exactly 50 domains (single rule)",
|
||||||
|
domainCount: 50,
|
||||||
|
expectedRuleCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "51 domains (two rules)",
|
||||||
|
domainCount: 51,
|
||||||
|
expectedRuleCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "100 domains (two rules)",
|
||||||
|
domainCount: 100,
|
||||||
|
expectedRuleCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "125 domains (three rules: 50+50+25)",
|
||||||
|
domainCount: 125,
|
||||||
|
expectedRuleCount: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// Clean up before each subtest
|
||||||
|
cleanupRegistryKeys(t)
|
||||||
|
|
||||||
|
// Generate domains
|
||||||
|
domains := make([]DomainConfig, tc.domainCount)
|
||||||
|
for i := 0; i < tc.domainCount; i++ {
|
||||||
|
domains[i] = DomainConfig{
|
||||||
|
Domain: fmt.Sprintf("domain%d.com", i+1),
|
||||||
|
MatchOnly: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config := HostDNSConfig{
|
||||||
|
ServerIP: testIP,
|
||||||
|
Domains: domains,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cfg.applyDNSConfig(config, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify that exactly expectedRuleCount rules were created
|
||||||
|
assert.Equal(t, tc.expectedRuleCount, cfg.nrptEntryCount,
|
||||||
|
"Should create %d NRPT rules for %d domains", tc.expectedRuleCount, tc.domainCount)
|
||||||
|
|
||||||
|
// Verify all expected rules exist
|
||||||
|
for i := 0; i < tc.expectedRuleCount; i++ {
|
||||||
|
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, exists, "NRPT rule %d should exist", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no extra rules were created
|
||||||
|
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, tc.expectedRuleCount))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, exists, "No NRPT rule should exist at index %d", tc.expectedRuleCount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -376,9 +376,9 @@ func (m *Resolver) extractDomainsFromServerDomains(serverDomains dnsconfig.Serve
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if serverDomains.Flow != "" {
|
// Flow receiver domain is intentionally excluded from caching.
|
||||||
domains = append(domains, serverDomains.Flow)
|
// Cloud providers may rotate the IP behind this domain; a stale cached record
|
||||||
}
|
// causes TLS certificate verification failures on reconnect.
|
||||||
|
|
||||||
for _, stun := range serverDomains.Stuns {
|
for _, stun := range serverDomains.Stuns {
|
||||||
if stun != "" {
|
if stun != "" {
|
||||||
|
|||||||
@@ -391,7 +391,8 @@ func TestResolver_PartialUpdateAddsNewTypePreservesExisting(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.Len(t, resolver.GetCachedDomains(), 3)
|
assert.Len(t, resolver.GetCachedDomains(), 3)
|
||||||
|
|
||||||
// Update with partial ServerDomains (only flow domain - new type, should preserve all existing)
|
// Update with partial ServerDomains (only flow domain - flow is intentionally excluded from
|
||||||
|
// caching to prevent TLS failures from stale records, so all existing domains are preserved)
|
||||||
partialDomains := dnsconfig.ServerDomains{
|
partialDomains := dnsconfig.ServerDomains{
|
||||||
Flow: "github.com",
|
Flow: "github.com",
|
||||||
}
|
}
|
||||||
@@ -400,10 +401,10 @@ func TestResolver_PartialUpdateAddsNewTypePreservesExisting(t *testing.T) {
|
|||||||
t.Skipf("Skipping test due to DNS resolution failure: %v", err)
|
t.Skipf("Skipping test due to DNS resolution failure: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Len(t, removedDomains, 0, "Should not remove any domains when adding new type")
|
assert.Len(t, removedDomains, 0, "Should not remove any domains when only flow domain is provided")
|
||||||
|
|
||||||
finalDomains := resolver.GetCachedDomains()
|
finalDomains := resolver.GetCachedDomains()
|
||||||
assert.Len(t, finalDomains, 4, "Should have all original domains plus new flow domain")
|
assert.Len(t, finalDomains, 3, "Flow domain is not cached; all original domains should be preserved")
|
||||||
|
|
||||||
domainStrings := make([]string, len(finalDomains))
|
domainStrings := make([]string, len(finalDomains))
|
||||||
for i, d := range finalDomains {
|
for i, d := range finalDomains {
|
||||||
@@ -412,5 +413,5 @@ func TestResolver_PartialUpdateAddsNewTypePreservesExisting(t *testing.T) {
|
|||||||
assert.Contains(t, domainStrings, "example.org")
|
assert.Contains(t, domainStrings, "example.org")
|
||||||
assert.Contains(t, domainStrings, "google.com")
|
assert.Contains(t, domainStrings, "google.com")
|
||||||
assert.Contains(t, domainStrings, "cloudflare.com")
|
assert.Contains(t, domainStrings, "cloudflare.com")
|
||||||
assert.Contains(t, domainStrings, "github.com")
|
assert.NotContains(t, domainStrings, "github.com")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,3 +84,18 @@ func (m *MockServer) UpdateServerConfig(domains dnsconfig.ServerDomains) error {
|
|||||||
func (m *MockServer) PopulateManagementDomain(mgmtURL *url.URL) error {
|
func (m *MockServer) PopulateManagementDomain(mgmtURL *url.URL) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeginBatch mock implementation of BeginBatch from Server interface
|
||||||
|
func (m *MockServer) BeginBatch() {
|
||||||
|
// Mock implementation - no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndBatch mock implementation of EndBatch from Server interface
|
||||||
|
func (m *MockServer) EndBatch() {
|
||||||
|
// Mock implementation - no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelBatch mock implementation of CancelBatch from Server interface
|
||||||
|
func (m *MockServer) CancelBatch() {
|
||||||
|
// Mock implementation - no-op
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ type IosDnsManager interface {
|
|||||||
type Server interface {
|
type Server interface {
|
||||||
RegisterHandler(domains domain.List, handler dns.Handler, priority int)
|
RegisterHandler(domains domain.List, handler dns.Handler, priority int)
|
||||||
DeregisterHandler(domains domain.List, priority int)
|
DeregisterHandler(domains domain.List, priority int)
|
||||||
|
BeginBatch()
|
||||||
|
EndBatch()
|
||||||
|
CancelBatch()
|
||||||
Initialize() error
|
Initialize() error
|
||||||
Stop()
|
Stop()
|
||||||
DnsIP() netip.Addr
|
DnsIP() netip.Addr
|
||||||
@@ -87,6 +90,7 @@ type DefaultServer struct {
|
|||||||
currentConfigHash uint64
|
currentConfigHash uint64
|
||||||
handlerChain *HandlerChain
|
handlerChain *HandlerChain
|
||||||
extraDomains map[domain.Domain]int
|
extraDomains map[domain.Domain]int
|
||||||
|
batchMode bool
|
||||||
|
|
||||||
mgmtCacheResolver *mgmt.Resolver
|
mgmtCacheResolver *mgmt.Resolver
|
||||||
|
|
||||||
@@ -234,7 +238,9 @@ func (s *DefaultServer) RegisterHandler(domains domain.List, handler dns.Handler
|
|||||||
// convert to zone with simple ref counter
|
// convert to zone with simple ref counter
|
||||||
s.extraDomains[toZone(domain)]++
|
s.extraDomains[toZone(domain)]++
|
||||||
}
|
}
|
||||||
s.applyHostConfig()
|
if !s.batchMode {
|
||||||
|
s.applyHostConfig()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, priority int) {
|
func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, priority int) {
|
||||||
@@ -263,9 +269,41 @@ func (s *DefaultServer) DeregisterHandler(domains domain.List, priority int) {
|
|||||||
delete(s.extraDomains, zone)
|
delete(s.extraDomains, zone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !s.batchMode {
|
||||||
|
s.applyHostConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeginBatch starts batch mode for DNS handler registration/deregistration.
|
||||||
|
// In batch mode, applyHostConfig() is not called after each handler operation,
|
||||||
|
// allowing multiple handlers to be registered/deregistered efficiently.
|
||||||
|
// Must be followed by EndBatch() to apply the accumulated changes.
|
||||||
|
func (s *DefaultServer) BeginBatch() {
|
||||||
|
s.mux.Lock()
|
||||||
|
defer s.mux.Unlock()
|
||||||
|
log.Debugf("DNS batch mode enabled")
|
||||||
|
s.batchMode = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndBatch ends batch mode and applies all accumulated DNS configuration changes.
|
||||||
|
func (s *DefaultServer) EndBatch() {
|
||||||
|
s.mux.Lock()
|
||||||
|
defer s.mux.Unlock()
|
||||||
|
log.Debugf("DNS batch mode disabled, applying accumulated changes")
|
||||||
|
s.batchMode = false
|
||||||
s.applyHostConfig()
|
s.applyHostConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CancelBatch cancels batch mode without applying accumulated changes.
|
||||||
|
// This is useful when operations fail partway through and you want to
|
||||||
|
// discard partial state rather than applying it.
|
||||||
|
func (s *DefaultServer) CancelBatch() {
|
||||||
|
s.mux.Lock()
|
||||||
|
defer s.mux.Unlock()
|
||||||
|
log.Debugf("DNS batch mode cancelled, discarding accumulated changes")
|
||||||
|
s.batchMode = false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) deregisterHandler(domains []string, priority int) {
|
func (s *DefaultServer) deregisterHandler(domains []string, priority int) {
|
||||||
log.Debugf("deregistering handler with priority %d for %v", priority, domains)
|
log.Debugf("deregistering handler with priority %d for %v", priority, domains)
|
||||||
|
|
||||||
@@ -523,6 +561,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
|
|||||||
s.currentConfig.RouteAll = false
|
s.currentConfig.RouteAll = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always apply host config for management updates, regardless of batch mode
|
||||||
s.applyHostConfig()
|
s.applyHostConfig()
|
||||||
|
|
||||||
s.shutdownWg.Add(1)
|
s.shutdownWg.Add(1)
|
||||||
@@ -887,6 +926,7 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always apply host config when nameserver goes down, regardless of batch mode
|
||||||
s.applyHostConfig()
|
s.applyHostConfig()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -922,6 +962,7 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
s.registerHandler([]string{nbdns.RootZone}, handler, priority)
|
s.registerHandler([]string{nbdns.RootZone}, handler, priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always apply host config when nameserver reactivates, regardless of batch mode
|
||||||
s.applyHostConfig()
|
s.applyHostConfig()
|
||||||
|
|
||||||
s.updateNSState(nsGroup, nil, true)
|
s.updateNSState(nsGroup, nil, true)
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ func TestGetServerDns(t *testing.T) {
|
|||||||
t.Errorf("invalid dns server instance: %s", err)
|
t.Errorf("invalid dns server instance: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if srvB != srv {
|
mockSrvB, ok := srvB.(*MockServer)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("returned server is not a MockServer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mockSrvB != srv {
|
||||||
t.Errorf("mismatch dns instances")
|
t.Errorf("mismatch dns instances")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,9 +351,13 @@ func (u *upstreamResolverBase) waitUntilResponse() {
|
|||||||
return fmt.Errorf("upstream check call error")
|
return fmt.Errorf("upstream check call error")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := backoff.Retry(operation, exponentialBackOff)
|
err := backoff.Retry(operation, backoff.WithContext(exponentialBackOff, u.ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn(err)
|
if errors.Is(err, context.Canceled) {
|
||||||
|
log.Debugf("upstream retry loop exited for upstreams %s", u.upstreamServersString())
|
||||||
|
} else {
|
||||||
|
log.Warnf("upstream retry loop exited for upstreams %s: %v", u.upstreamServersString(), err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,14 @@ import (
|
|||||||
firewallManager "github.com/netbirdio/netbird/client/firewall/manager"
|
firewallManager "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
"github.com/netbirdio/netbird/client/iface/udpmux"
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/internal/acl"
|
"github.com/netbirdio/netbird/client/internal/acl"
|
||||||
"github.com/netbirdio/netbird/client/internal/debug"
|
"github.com/netbirdio/netbird/client/internal/debug"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns"
|
"github.com/netbirdio/netbird/client/internal/dns"
|
||||||
dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config"
|
dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config"
|
||||||
"github.com/netbirdio/netbird/client/internal/dnsfwd"
|
"github.com/netbirdio/netbird/client/internal/dnsfwd"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/expose"
|
||||||
"github.com/netbirdio/netbird/client/internal/ingressgw"
|
"github.com/netbirdio/netbird/client/internal/ingressgw"
|
||||||
"github.com/netbirdio/netbird/client/internal/netflow"
|
"github.com/netbirdio/netbird/client/internal/netflow"
|
||||||
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
|
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
|
||||||
@@ -52,13 +54,11 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/updatemanager"
|
"github.com/netbirdio/netbird/client/internal/updatemanager"
|
||||||
"github.com/netbirdio/netbird/client/jobexec"
|
"github.com/netbirdio/netbird/client/jobexec"
|
||||||
cProto "github.com/netbirdio/netbird/client/proto"
|
cProto "github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
|
||||||
semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
mgm "github.com/netbirdio/netbird/shared/management/client"
|
mgm "github.com/netbirdio/netbird/shared/management/client"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
auth "github.com/netbirdio/netbird/shared/relay/auth/hmac"
|
auth "github.com/netbirdio/netbird/shared/relay/auth/hmac"
|
||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
@@ -74,7 +74,6 @@ import (
|
|||||||
const (
|
const (
|
||||||
PeerConnectionTimeoutMax = 45000 // ms
|
PeerConnectionTimeoutMax = 45000 // ms
|
||||||
PeerConnectionTimeoutMin = 30000 // ms
|
PeerConnectionTimeoutMin = 30000 // ms
|
||||||
connInitLimit = 200
|
|
||||||
disableAutoUpdate = "disabled"
|
disableAutoUpdate = "disabled"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -207,7 +206,6 @@ type Engine struct {
|
|||||||
syncRespMux sync.RWMutex
|
syncRespMux sync.RWMutex
|
||||||
persistSyncResponse bool
|
persistSyncResponse bool
|
||||||
latestSyncResponse *mgmProto.SyncResponse
|
latestSyncResponse *mgmProto.SyncResponse
|
||||||
connSemaphore *semaphoregroup.SemaphoreGroup
|
|
||||||
flowManager nftypes.FlowManager
|
flowManager nftypes.FlowManager
|
||||||
|
|
||||||
// auto-update
|
// auto-update
|
||||||
@@ -223,6 +221,8 @@ type Engine struct {
|
|||||||
|
|
||||||
jobExecutor *jobexec.Executor
|
jobExecutor *jobexec.Executor
|
||||||
jobExecutorWG sync.WaitGroup
|
jobExecutorWG sync.WaitGroup
|
||||||
|
|
||||||
|
exposeManager *expose.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
// Peer is an instance of the Connection Peer
|
// Peer is an instance of the Connection Peer
|
||||||
@@ -265,7 +265,6 @@ func NewEngine(
|
|||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
stateManager: stateManager,
|
stateManager: stateManager,
|
||||||
checks: checks,
|
checks: checks,
|
||||||
connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit),
|
|
||||||
probeStunTurn: relay.NewStunTurnProbe(relay.DefaultCacheTTL),
|
probeStunTurn: relay.NewStunTurnProbe(relay.DefaultCacheTTL),
|
||||||
jobExecutor: jobexec.NewExecutor(),
|
jobExecutor: jobexec.NewExecutor(),
|
||||||
}
|
}
|
||||||
@@ -418,6 +417,7 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
|||||||
e.cancel()
|
e.cancel()
|
||||||
}
|
}
|
||||||
e.ctx, e.cancel = context.WithCancel(e.clientCtx)
|
e.ctx, e.cancel = context.WithCancel(e.clientCtx)
|
||||||
|
e.exposeManager = expose.NewManager(e.ctx, e.mgmClient)
|
||||||
|
|
||||||
wgIface, err := e.newWgIface()
|
wgIface, err := e.newWgIface()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -800,7 +800,7 @@ func (e *Engine) handleAutoUpdateVersion(autoUpdateSettings *mgmProto.AutoUpdate
|
|||||||
|
|
||||||
disabled := autoUpdateSettings.Version == disableAutoUpdate
|
disabled := autoUpdateSettings.Version == disableAutoUpdate
|
||||||
|
|
||||||
// Stop and cleanup if disabled
|
// stop and cleanup if disabled
|
||||||
if e.updateManager != nil && disabled {
|
if e.updateManager != nil && disabled {
|
||||||
log.Infof("auto-update is disabled, stopping update manager")
|
log.Infof("auto-update is disabled, stopping update manager")
|
||||||
e.updateManager.Stop()
|
e.updateManager.Stop()
|
||||||
@@ -1538,7 +1538,6 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentV
|
|||||||
IFaceDiscover: e.mobileDep.IFaceDiscover,
|
IFaceDiscover: e.mobileDep.IFaceDiscover,
|
||||||
RelayManager: e.relayManager,
|
RelayManager: e.relayManager,
|
||||||
SrWatcher: e.srWatcher,
|
SrWatcher: e.srWatcher,
|
||||||
Semaphore: e.connSemaphore,
|
|
||||||
}
|
}
|
||||||
peerConn, err := peer.NewConn(config, serviceDependencies)
|
peerConn, err := peer.NewConn(config, serviceDependencies)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1561,8 +1560,10 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
defer e.shutdownWg.Done()
|
defer e.shutdownWg.Done()
|
||||||
// connect to a stream of messages coming from the signal server
|
// connect to a stream of messages coming from the signal server
|
||||||
err := e.signal.Receive(e.ctx, func(msg *sProto.Message) error {
|
err := e.signal.Receive(e.ctx, func(msg *sProto.Message) error {
|
||||||
|
start := time.Now()
|
||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
|
gotLock := time.Since(start)
|
||||||
|
|
||||||
// Check context INSIDE lock to ensure atomicity with shutdown
|
// Check context INSIDE lock to ensure atomicity with shutdown
|
||||||
if e.ctx.Err() != nil {
|
if e.ctx.Err() != nil {
|
||||||
@@ -1586,6 +1587,8 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debugf("receiveMSG: took %s to get lock for peer %s with session id %s", gotLock, msg.Key, offerAnswer.SessionID)
|
||||||
|
|
||||||
if msg.Body.Type == sProto.Body_OFFER {
|
if msg.Body.Type == sProto.Body_OFFER {
|
||||||
conn.OnRemoteOffer(*offerAnswer)
|
conn.OnRemoteOffer(*offerAnswer)
|
||||||
} else {
|
} else {
|
||||||
@@ -1819,11 +1822,18 @@ func (e *Engine) GetRouteManager() routemanager.Manager {
|
|||||||
return e.routeManager
|
return e.routeManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFirewallManager returns the firewall manager
|
// GetFirewallManager returns the firewall manager.
|
||||||
func (e *Engine) GetFirewallManager() firewallManager.Manager {
|
func (e *Engine) GetFirewallManager() firewallManager.Manager {
|
||||||
return e.firewall
|
return e.firewall
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetExposeManager returns the expose session manager.
|
||||||
|
func (e *Engine) GetExposeManager() *expose.Manager {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
return e.exposeManager
|
||||||
|
}
|
||||||
|
|
||||||
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
|
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
|
||||||
iface, err := net.InterfaceByName(ifaceName)
|
iface, err := net.InterfaceByName(ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1923,7 +1933,7 @@ func (e *Engine) triggerClientRestart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) startNetworkMonitor() {
|
func (e *Engine) startNetworkMonitor() {
|
||||||
if !e.config.NetworkMonitor {
|
if !e.config.NetworkMonitor || nbnetstack.IsEnabled() {
|
||||||
log.Infof("Network monitor is disabled, not starting")
|
log.Infof("Network monitor is disabled, not starting")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
95
client/internal/expose/manager.go
Normal file
95
client/internal/expose/manager.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package expose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mgm "github.com/netbirdio/netbird/shared/management/client"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const renewTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
// Response holds the response from exposing a service.
|
||||||
|
type Response struct {
|
||||||
|
ServiceName string
|
||||||
|
ServiceURL string
|
||||||
|
Domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
NamePrefix string
|
||||||
|
Domain string
|
||||||
|
Port uint16
|
||||||
|
Protocol int
|
||||||
|
Pin string
|
||||||
|
Password string
|
||||||
|
UserGroups []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManagementClient interface {
|
||||||
|
CreateExpose(ctx context.Context, req mgm.ExposeRequest) (*mgm.ExposeResponse, error)
|
||||||
|
RenewExpose(ctx context.Context, domain string) error
|
||||||
|
StopExpose(ctx context.Context, domain string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manager handles expose session lifecycle via the management client.
|
||||||
|
type Manager struct {
|
||||||
|
mgmClient ManagementClient
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager creates a new expose Manager using the given management client.
|
||||||
|
func NewManager(ctx context.Context, mgmClient ManagementClient) *Manager {
|
||||||
|
return &Manager{mgmClient: mgmClient, ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose creates a new expose session via the management server.
|
||||||
|
func (m *Manager) Expose(ctx context.Context, req Request) (*Response, error) {
|
||||||
|
log.Infof("exposing service on port %d", req.Port)
|
||||||
|
resp, err := m.mgmClient.CreateExpose(ctx, toClientExposeRequest(req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("expose session created for %s", resp.Domain)
|
||||||
|
|
||||||
|
return fromClientExposeResponse(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) KeepAlive(ctx context.Context, domain string) error {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
defer m.stop(domain)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Infof("context canceled, stopping keep alive for %s", domain)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := m.renew(ctx, domain); err != nil {
|
||||||
|
log.Errorf("renewing expose session for %s: %v", domain, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// renew extends the TTL of an active expose session.
|
||||||
|
func (m *Manager) renew(ctx context.Context, domain string) error {
|
||||||
|
renewCtx, cancel := context.WithTimeout(ctx, renewTimeout)
|
||||||
|
defer cancel()
|
||||||
|
return m.mgmClient.RenewExpose(renewCtx, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop terminates an active expose session.
|
||||||
|
func (m *Manager) stop(domain string) {
|
||||||
|
stopCtx, cancel := context.WithTimeout(m.ctx, renewTimeout)
|
||||||
|
defer cancel()
|
||||||
|
err := m.mgmClient.StopExpose(stopCtx, domain)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed stopping expose session for %s: %v", domain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
95
client/internal/expose/manager_test.go
Normal file
95
client/internal/expose/manager_test.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package expose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
daemonProto "github.com/netbirdio/netbird/client/proto"
|
||||||
|
mgm "github.com/netbirdio/netbird/shared/management/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestManager_Expose_Success(t *testing.T) {
|
||||||
|
mock := &mgm.MockClient{
|
||||||
|
CreateExposeFunc: func(ctx context.Context, req mgm.ExposeRequest) (*mgm.ExposeResponse, error) {
|
||||||
|
return &mgm.ExposeResponse{
|
||||||
|
ServiceName: "my-service",
|
||||||
|
ServiceURL: "https://my-service.example.com",
|
||||||
|
Domain: "my-service.example.com",
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewManager(context.Background(), mock)
|
||||||
|
result, err := m.Expose(context.Background(), Request{Port: 8080})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "my-service", result.ServiceName, "service name should match")
|
||||||
|
assert.Equal(t, "https://my-service.example.com", result.ServiceURL, "service URL should match")
|
||||||
|
assert.Equal(t, "my-service.example.com", result.Domain, "domain should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_Expose_Error(t *testing.T) {
|
||||||
|
mock := &mgm.MockClient{
|
||||||
|
CreateExposeFunc: func(ctx context.Context, req mgm.ExposeRequest) (*mgm.ExposeResponse, error) {
|
||||||
|
return nil, errors.New("permission denied")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewManager(context.Background(), mock)
|
||||||
|
_, err := m.Expose(context.Background(), Request{Port: 8080})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "permission denied", "error should propagate")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_Renew_Success(t *testing.T) {
|
||||||
|
mock := &mgm.MockClient{
|
||||||
|
RenewExposeFunc: func(ctx context.Context, domain string) error {
|
||||||
|
assert.Equal(t, "my-service.example.com", domain, "domain should be passed through")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewManager(context.Background(), mock)
|
||||||
|
err := m.renew(context.Background(), "my-service.example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_Renew_Timeout(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
mock := &mgm.MockClient{
|
||||||
|
RenewExposeFunc: func(ctx context.Context, domain string) error {
|
||||||
|
return ctx.Err()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewManager(ctx, mock)
|
||||||
|
err := m.renew(ctx, "my-service.example.com")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRequest(t *testing.T) {
|
||||||
|
req := &daemonProto.ExposeServiceRequest{
|
||||||
|
Port: 8080,
|
||||||
|
Protocol: daemonProto.ExposeProtocol_EXPOSE_HTTPS,
|
||||||
|
Pin: "123456",
|
||||||
|
Password: "secret",
|
||||||
|
UserGroups: []string{"group1", "group2"},
|
||||||
|
Domain: "custom.example.com",
|
||||||
|
NamePrefix: "my-prefix",
|
||||||
|
}
|
||||||
|
|
||||||
|
exposeReq := NewRequest(req)
|
||||||
|
|
||||||
|
assert.Equal(t, uint16(8080), exposeReq.Port, "port should match")
|
||||||
|
assert.Equal(t, int(daemonProto.ExposeProtocol_EXPOSE_HTTPS), exposeReq.Protocol, "protocol should match")
|
||||||
|
assert.Equal(t, "123456", exposeReq.Pin, "pin should match")
|
||||||
|
assert.Equal(t, "secret", exposeReq.Password, "password should match")
|
||||||
|
assert.Equal(t, []string{"group1", "group2"}, exposeReq.UserGroups, "user groups should match")
|
||||||
|
assert.Equal(t, "custom.example.com", exposeReq.Domain, "domain should match")
|
||||||
|
assert.Equal(t, "my-prefix", exposeReq.NamePrefix, "name prefix should match")
|
||||||
|
}
|
||||||
39
client/internal/expose/request.go
Normal file
39
client/internal/expose/request.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package expose
|
||||||
|
|
||||||
|
import (
|
||||||
|
daemonProto "github.com/netbirdio/netbird/client/proto"
|
||||||
|
mgm "github.com/netbirdio/netbird/shared/management/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRequest converts a daemon ExposeServiceRequest to a management ExposeServiceRequest.
|
||||||
|
func NewRequest(req *daemonProto.ExposeServiceRequest) *Request {
|
||||||
|
return &Request{
|
||||||
|
Port: uint16(req.Port),
|
||||||
|
Protocol: int(req.Protocol),
|
||||||
|
Pin: req.Pin,
|
||||||
|
Password: req.Password,
|
||||||
|
UserGroups: req.UserGroups,
|
||||||
|
Domain: req.Domain,
|
||||||
|
NamePrefix: req.NamePrefix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toClientExposeRequest(req Request) mgm.ExposeRequest {
|
||||||
|
return mgm.ExposeRequest{
|
||||||
|
NamePrefix: req.NamePrefix,
|
||||||
|
Domain: req.Domain,
|
||||||
|
Port: req.Port,
|
||||||
|
Protocol: req.Protocol,
|
||||||
|
Pin: req.Pin,
|
||||||
|
Password: req.Password,
|
||||||
|
UserGroups: req.UserGroups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromClientExposeResponse(response *mgm.ExposeResponse) *Response {
|
||||||
|
return &Response{
|
||||||
|
ServiceName: response.ServiceName,
|
||||||
|
Domain: response.Domain,
|
||||||
|
ServiceURL: response.ServiceURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,51 +22,56 @@ func prepareFd() (int, error) {
|
|||||||
|
|
||||||
func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Nexthop) error {
|
func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Nexthop) error {
|
||||||
for {
|
for {
|
||||||
select {
|
// Wait until fd is readable or context is cancelled, to avoid a busy-loop
|
||||||
case <-ctx.Done():
|
// when the routing socket returns EAGAIN (e.g. immediately after wakeup).
|
||||||
return ctx.Err()
|
if err := waitReadable(ctx, fd); err != nil {
|
||||||
default:
|
return err
|
||||||
buf := make([]byte, 2048)
|
}
|
||||||
n, err := unix.Read(fd, buf)
|
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
n, err := unix.Read(fd, buf)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if errors.Is(err, unix.EBADF) || errors.Is(err, unix.EINVAL) {
|
||||||
|
return fmt.Errorf("routing socket closed: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("read routing socket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < unix.SizeofRtMsghdr {
|
||||||
|
log.Debugf("Network monitor: read from routing socket returned less than expected: %d bytes", n)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := (*unix.RtMsghdr)(unsafe.Pointer(&buf[0]))
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
// handle route changes
|
||||||
|
case unix.RTM_ADD, syscall.RTM_DELETE:
|
||||||
|
route, err := parseRouteMessage(buf[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, unix.EBADF) && !errors.Is(err, unix.EINVAL) {
|
log.Debugf("Network monitor: error parsing routing message: %v", err)
|
||||||
log.Warnf("Network monitor: failed to read from routing socket: %v", err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if n < unix.SizeofRtMsghdr {
|
|
||||||
log.Debugf("Network monitor: read from routing socket returned less than expected: %d bytes", n)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := (*unix.RtMsghdr)(unsafe.Pointer(&buf[0]))
|
if route.Dst.Bits() != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
intf := "<nil>"
|
||||||
|
if route.Interface != nil {
|
||||||
|
intf = route.Interface.Name
|
||||||
|
}
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
// handle route changes
|
case unix.RTM_ADD:
|
||||||
case unix.RTM_ADD, syscall.RTM_DELETE:
|
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
|
||||||
route, err := parseRouteMessage(buf[:n])
|
return nil
|
||||||
if err != nil {
|
case unix.RTM_DELETE:
|
||||||
log.Debugf("Network monitor: error parsing routing message: %v", err)
|
if nexthopv4.Intf != nil && route.Gw.Compare(nexthopv4.IP) == 0 || nexthopv6.Intf != nil && route.Gw.Compare(nexthopv6.IP) == 0 {
|
||||||
continue
|
log.Infof("Network monitor: default route removed: via %s, interface %s", route.Gw, intf)
|
||||||
}
|
|
||||||
|
|
||||||
if route.Dst.Bits() != 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
intf := "<nil>"
|
|
||||||
if route.Interface != nil {
|
|
||||||
intf = route.Interface.Name
|
|
||||||
}
|
|
||||||
switch msg.Type {
|
|
||||||
case unix.RTM_ADD:
|
|
||||||
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
|
|
||||||
return nil
|
return nil
|
||||||
case unix.RTM_DELETE:
|
|
||||||
if nexthopv4.Intf != nil && route.Gw.Compare(nexthopv4.IP) == 0 || nexthopv6.Intf != nil && route.Gw.Compare(nexthopv6.IP) == 0 {
|
|
||||||
log.Infof("Network monitor: default route removed: via %s, interface %s", route.Gw, intf)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,3 +95,33 @@ func parseRouteMessage(buf []byte) (*systemops.Route, error) {
|
|||||||
|
|
||||||
return systemops.MsgToRoute(msg)
|
return systemops.MsgToRoute(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitReadable blocks until fd has data to read, or ctx is cancelled.
|
||||||
|
func waitReadable(ctx context.Context, fd int) error {
|
||||||
|
var fdset unix.FdSet
|
||||||
|
if fd < 0 || fd/unix.NFDBITS >= len(fdset.Bits) {
|
||||||
|
return fmt.Errorf("fd %d out of range for FdSet", fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fdset = unix.FdSet{}
|
||||||
|
fdset.Set(fd)
|
||||||
|
// Use a 1-second timeout so we can re-check ctx periodically.
|
||||||
|
tv := unix.Timeval{Sec: 1}
|
||||||
|
n, err := unix.Select(fd+1, &fdset, nil, nil, &tv)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, unix.EINTR) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("select on routing socket: %w", err)
|
||||||
|
}
|
||||||
|
if n > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// timeout — loop back and re-check ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/netstack"
|
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,11 +37,6 @@ func New() *NetworkMonitor {
|
|||||||
|
|
||||||
// Listen begins monitoring network changes. When a change is detected, this function will return without error.
|
// Listen begins monitoring network changes. When a change is detected, this function will return without error.
|
||||||
func (nw *NetworkMonitor) Listen(ctx context.Context) (err error) {
|
func (nw *NetworkMonitor) Listen(ctx context.Context) (err error) {
|
||||||
if netstack.IsEnabled() {
|
|
||||||
log.Debugf("Network monitor: skipping in netstack mode")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
nw.mu.Lock()
|
nw.mu.Lock()
|
||||||
if nw.cancel != nil {
|
if nw.cancel != nil {
|
||||||
nw.mu.Unlock()
|
nw.mu.Unlock()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package peer
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -25,7 +24,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServiceDependencies struct {
|
type ServiceDependencies struct {
|
||||||
@@ -34,7 +32,6 @@ type ServiceDependencies struct {
|
|||||||
IFaceDiscover stdnet.ExternalIFaceDiscover
|
IFaceDiscover stdnet.ExternalIFaceDiscover
|
||||||
RelayManager *relayClient.Manager
|
RelayManager *relayClient.Manager
|
||||||
SrWatcher *guard.SRWatcher
|
SrWatcher *guard.SRWatcher
|
||||||
Semaphore *semaphoregroup.SemaphoreGroup
|
|
||||||
PeerConnDispatcher *dispatcher.ConnectionDispatcher
|
PeerConnDispatcher *dispatcher.ConnectionDispatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,9 +108,8 @@ type Conn struct {
|
|||||||
wgProxyRelay wgproxy.Proxy
|
wgProxyRelay wgproxy.Proxy
|
||||||
handshaker *Handshaker
|
handshaker *Handshaker
|
||||||
|
|
||||||
guard *guard.Guard
|
guard *guard.Guard
|
||||||
semaphore *semaphoregroup.SemaphoreGroup
|
wg sync.WaitGroup
|
||||||
wg sync.WaitGroup
|
|
||||||
|
|
||||||
// debug purpose
|
// debug purpose
|
||||||
dumpState *stateDump
|
dumpState *stateDump
|
||||||
@@ -139,7 +135,6 @@ func NewConn(config ConnConfig, services ServiceDependencies) (*Conn, error) {
|
|||||||
iFaceDiscover: services.IFaceDiscover,
|
iFaceDiscover: services.IFaceDiscover,
|
||||||
relayManager: services.RelayManager,
|
relayManager: services.RelayManager,
|
||||||
srWatcher: services.SrWatcher,
|
srWatcher: services.SrWatcher,
|
||||||
semaphore: services.Semaphore,
|
|
||||||
statusRelay: worker.NewAtomicStatus(),
|
statusRelay: worker.NewAtomicStatus(),
|
||||||
statusICE: worker.NewAtomicStatus(),
|
statusICE: worker.NewAtomicStatus(),
|
||||||
dumpState: dumpState,
|
dumpState: dumpState,
|
||||||
@@ -154,15 +149,10 @@ func NewConn(config ConnConfig, services ServiceDependencies) (*Conn, error) {
|
|||||||
// It will try to establish a connection using ICE and in parallel with relay. The higher priority connection type will
|
// It will try to establish a connection using ICE and in parallel with relay. The higher priority connection type will
|
||||||
// be used.
|
// be used.
|
||||||
func (conn *Conn) Open(engineCtx context.Context) error {
|
func (conn *Conn) Open(engineCtx context.Context) error {
|
||||||
if err := conn.semaphore.Add(engineCtx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.mu.Lock()
|
conn.mu.Lock()
|
||||||
defer conn.mu.Unlock()
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
if conn.opened {
|
if conn.opened {
|
||||||
conn.semaphore.Done()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +163,6 @@ func (conn *Conn) Open(engineCtx context.Context) error {
|
|||||||
relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally()
|
relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally()
|
||||||
workerICE, err := NewWorkerICE(conn.ctx, conn.Log, conn.config, conn, conn.signaler, conn.iFaceDiscover, conn.statusRecorder, relayIsSupportedLocally)
|
workerICE, err := NewWorkerICE(conn.ctx, conn.Log, conn.config, conn, conn.signaler, conn.iFaceDiscover, conn.statusRecorder, relayIsSupportedLocally)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.semaphore.Done()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn.workerICE = workerICE
|
conn.workerICE = workerICE
|
||||||
@@ -207,10 +196,6 @@ func (conn *Conn) Open(engineCtx context.Context) error {
|
|||||||
conn.wg.Add(1)
|
conn.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer conn.wg.Done()
|
defer conn.wg.Done()
|
||||||
|
|
||||||
conn.waitInitialRandomSleepTime(conn.ctx)
|
|
||||||
conn.semaphore.Done()
|
|
||||||
|
|
||||||
conn.guard.Start(conn.ctx, conn.onGuardEvent)
|
conn.guard.Start(conn.ctx, conn.onGuardEvent)
|
||||||
}()
|
}()
|
||||||
conn.opened = true
|
conn.opened = true
|
||||||
@@ -410,7 +395,7 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn
|
|||||||
conn.doOnConnected(iceConnInfo.RosenpassPubKey, iceConnInfo.RosenpassAddr)
|
conn.doOnConnected(iceConnInfo.RosenpassPubKey, iceConnInfo.RosenpassAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) onICEStateDisconnected() {
|
func (conn *Conn) onICEStateDisconnected(sessionChanged bool) {
|
||||||
conn.mu.Lock()
|
conn.mu.Lock()
|
||||||
defer conn.mu.Unlock()
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
@@ -430,14 +415,18 @@ func (conn *Conn) onICEStateDisconnected() {
|
|||||||
if conn.isReadyToUpgrade() {
|
if conn.isReadyToUpgrade() {
|
||||||
conn.Log.Infof("ICE disconnected, set Relay to active connection")
|
conn.Log.Infof("ICE disconnected, set Relay to active connection")
|
||||||
conn.dumpState.SwitchToRelay()
|
conn.dumpState.SwitchToRelay()
|
||||||
|
if sessionChanged {
|
||||||
|
conn.resetEndpoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo consider to move after the ConfigureWGEndpoint
|
||||||
conn.wgProxyRelay.Work()
|
conn.wgProxyRelay.Work()
|
||||||
|
|
||||||
presharedKey := conn.presharedKey(conn.rosenpassRemoteKey)
|
presharedKey := conn.presharedKey(conn.rosenpassRemoteKey)
|
||||||
if err := conn.endpointUpdater.ConfigureWGEndpoint(conn.wgProxyRelay.EndpointAddr(), presharedKey); err != nil {
|
if err := conn.endpointUpdater.SwitchWGEndpoint(conn.wgProxyRelay.EndpointAddr(), presharedKey); err != nil {
|
||||||
conn.Log.Errorf("failed to switch to relay conn: %v", err)
|
conn.Log.Errorf("failed to switch to relay conn: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.wgProxyRelay.Work()
|
|
||||||
conn.currentConnPriority = conntype.Relay
|
conn.currentConnPriority = conntype.Relay
|
||||||
} else {
|
} else {
|
||||||
conn.Log.Infof("ICE disconnected, do not switch to Relay. Reset priority to: %s", conntype.None.String())
|
conn.Log.Infof("ICE disconnected, do not switch to Relay. Reset priority to: %s", conntype.None.String())
|
||||||
@@ -499,20 +488,22 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wgProxy.Work()
|
controller := isController(conn.config)
|
||||||
presharedKey := conn.presharedKey(rci.rosenpassPubKey)
|
|
||||||
|
|
||||||
|
if controller {
|
||||||
|
wgProxy.Work()
|
||||||
|
}
|
||||||
conn.enableWgWatcherIfNeeded()
|
conn.enableWgWatcherIfNeeded()
|
||||||
|
if err := conn.endpointUpdater.ConfigureWGEndpoint(wgProxy.EndpointAddr(), conn.presharedKey(rci.rosenpassPubKey)); err != nil {
|
||||||
if err := conn.endpointUpdater.ConfigureWGEndpoint(wgProxy.EndpointAddr(), presharedKey); err != nil {
|
|
||||||
if err := wgProxy.CloseConn(); err != nil {
|
if err := wgProxy.CloseConn(); err != nil {
|
||||||
conn.Log.Warnf("Failed to close relay connection: %v", err)
|
conn.Log.Warnf("Failed to close relay connection: %v", err)
|
||||||
}
|
}
|
||||||
conn.Log.Errorf("Failed to update WireGuard peer configuration: %v", err)
|
conn.Log.Errorf("Failed to update WireGuard peer configuration: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !controller {
|
||||||
wgConfigWorkaround()
|
wgProxy.Work()
|
||||||
|
}
|
||||||
conn.rosenpassRemoteKey = rci.rosenpassPubKey
|
conn.rosenpassRemoteKey = rci.rosenpassPubKey
|
||||||
conn.currentConnPriority = conntype.Relay
|
conn.currentConnPriority = conntype.Relay
|
||||||
conn.statusRelay.SetConnected()
|
conn.statusRelay.SetConnected()
|
||||||
@@ -664,19 +655,6 @@ func (conn *Conn) doOnConnected(remoteRosenpassPubKey []byte, remoteRosenpassAdd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) waitInitialRandomSleepTime(ctx context.Context) {
|
|
||||||
maxWait := 300
|
|
||||||
duration := time.Duration(rand.Intn(maxWait)) * time.Millisecond
|
|
||||||
|
|
||||||
timeout := time.NewTimer(duration)
|
|
||||||
defer timeout.Stop()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case <-timeout.C:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (conn *Conn) isRelayed() bool {
|
func (conn *Conn) isRelayed() bool {
|
||||||
switch conn.currentConnPriority {
|
switch conn.currentConnPriority {
|
||||||
case conntype.Relay, conntype.ICETurn:
|
case conntype.Relay, conntype.ICETurn:
|
||||||
@@ -757,6 +735,17 @@ func (conn *Conn) newProxy(remoteConn net.Conn) (wgproxy.Proxy, error) {
|
|||||||
return wgProxy, nil
|
return wgProxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (conn *Conn) resetEndpoint() {
|
||||||
|
if !isController(conn.config) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn.Log.Infof("reset wg endpoint")
|
||||||
|
conn.wgWatcher.Reset()
|
||||||
|
if err := conn.endpointUpdater.RemoveEndpointAddress(); err != nil {
|
||||||
|
conn.Log.Warnf("failed to remove endpoint address before update: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (conn *Conn) isReadyToUpgrade() bool {
|
func (conn *Conn) isReadyToUpgrade() bool {
|
||||||
return conn.wgProxyRelay != nil && conn.currentConnPriority != conntype.Relay
|
return conn.wgProxyRelay != nil && conn.currentConnPriority != conntype.Relay
|
||||||
}
|
}
|
||||||
@@ -862,9 +851,3 @@ func isController(config ConnConfig) bool {
|
|||||||
func isRosenpassEnabled(remoteRosenpassPubKey []byte) bool {
|
func isRosenpassEnabled(remoteRosenpassPubKey []byte) bool {
|
||||||
return remoteRosenpassPubKey != nil
|
return remoteRosenpassPubKey != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// wgConfigWorkaround is a workaround for the issue with WireGuard configuration update
|
|
||||||
// When update a peer configuration in near to each other time, the second update can be ignored by WireGuard
|
|
||||||
func wgConfigWorkaround() {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/peer/ice"
|
"github.com/netbirdio/netbird/client/internal/peer/ice"
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var testDispatcher = dispatcher.NewConnectionDispatcher()
|
var testDispatcher = dispatcher.NewConnectionDispatcher()
|
||||||
@@ -53,7 +52,6 @@ func TestConn_GetKey(t *testing.T) {
|
|||||||
|
|
||||||
sd := ServiceDependencies{
|
sd := ServiceDependencies{
|
||||||
SrWatcher: swWatcher,
|
SrWatcher: swWatcher,
|
||||||
Semaphore: semaphoregroup.NewSemaphoreGroup(1),
|
|
||||||
PeerConnDispatcher: testDispatcher,
|
PeerConnDispatcher: testDispatcher,
|
||||||
}
|
}
|
||||||
conn, err := NewConn(connConf, sd)
|
conn, err := NewConn(connConf, sd)
|
||||||
@@ -71,7 +69,6 @@ func TestConn_OnRemoteOffer(t *testing.T) {
|
|||||||
sd := ServiceDependencies{
|
sd := ServiceDependencies{
|
||||||
StatusRecorder: NewRecorder("https://mgm"),
|
StatusRecorder: NewRecorder("https://mgm"),
|
||||||
SrWatcher: swWatcher,
|
SrWatcher: swWatcher,
|
||||||
Semaphore: semaphoregroup.NewSemaphoreGroup(1),
|
|
||||||
PeerConnDispatcher: testDispatcher,
|
PeerConnDispatcher: testDispatcher,
|
||||||
}
|
}
|
||||||
conn, err := NewConn(connConf, sd)
|
conn, err := NewConn(connConf, sd)
|
||||||
@@ -110,7 +107,6 @@ func TestConn_OnRemoteAnswer(t *testing.T) {
|
|||||||
sd := ServiceDependencies{
|
sd := ServiceDependencies{
|
||||||
StatusRecorder: NewRecorder("https://mgm"),
|
StatusRecorder: NewRecorder("https://mgm"),
|
||||||
SrWatcher: swWatcher,
|
SrWatcher: swWatcher,
|
||||||
Semaphore: semaphoregroup.NewSemaphoreGroup(1),
|
|
||||||
PeerConnDispatcher: testDispatcher,
|
PeerConnDispatcher: testDispatcher,
|
||||||
}
|
}
|
||||||
conn, err := NewConn(connConf, sd)
|
conn, err := NewConn(connConf, sd)
|
||||||
|
|||||||
@@ -34,28 +34,27 @@ func NewEndpointUpdater(log *logrus.Entry, wgConfig WgConfig, initiator bool) *E
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigureWGEndpoint sets up the WireGuard endpoint configuration.
|
|
||||||
// The initiator immediately configures the endpoint, while the non-initiator
|
|
||||||
// waits for a fallback period before configuring to avoid handshake congestion.
|
|
||||||
func (e *EndpointUpdater) ConfigureWGEndpoint(addr *net.UDPAddr, presharedKey *wgtypes.Key) error {
|
func (e *EndpointUpdater) ConfigureWGEndpoint(addr *net.UDPAddr, presharedKey *wgtypes.Key) error {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
if e.initiator {
|
if e.initiator {
|
||||||
e.log.Debugf("configure up WireGuard as initiatr")
|
e.log.Debugf("configure up WireGuard as initiator")
|
||||||
return e.updateWireGuardPeer(addr, presharedKey)
|
return e.configureAsInitiator(addr, presharedKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.log.Debugf("configure up WireGuard as responder")
|
||||||
|
return e.configureAsResponder(addr, presharedKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EndpointUpdater) SwitchWGEndpoint(addr *net.UDPAddr, presharedKey *wgtypes.Key) error {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
// prevent to run new update while cancel the previous update
|
// prevent to run new update while cancel the previous update
|
||||||
e.waitForCloseTheDelayedUpdate()
|
e.waitForCloseTheDelayedUpdate()
|
||||||
|
|
||||||
var ctx context.Context
|
return e.updateWireGuardPeer(addr, presharedKey)
|
||||||
ctx, e.cancelFunc = context.WithCancel(context.Background())
|
|
||||||
e.updateWg.Add(1)
|
|
||||||
go e.scheduleDelayedUpdate(ctx, addr, presharedKey)
|
|
||||||
|
|
||||||
e.log.Debugf("configure up WireGuard and wait for handshake")
|
|
||||||
return e.updateWireGuardPeer(nil, presharedKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EndpointUpdater) RemoveWgPeer() error {
|
func (e *EndpointUpdater) RemoveWgPeer() error {
|
||||||
@@ -66,6 +65,38 @@ func (e *EndpointUpdater) RemoveWgPeer() error {
|
|||||||
return e.wgConfig.WgInterface.RemovePeer(e.wgConfig.RemoteKey)
|
return e.wgConfig.WgInterface.RemovePeer(e.wgConfig.RemoteKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *EndpointUpdater) RemoveEndpointAddress() error {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
e.waitForCloseTheDelayedUpdate()
|
||||||
|
return e.wgConfig.WgInterface.RemoveEndpointAddress(e.wgConfig.RemoteKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EndpointUpdater) configureAsInitiator(addr *net.UDPAddr, presharedKey *wgtypes.Key) error {
|
||||||
|
if err := e.updateWireGuardPeer(addr, presharedKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EndpointUpdater) configureAsResponder(addr *net.UDPAddr, presharedKey *wgtypes.Key) error {
|
||||||
|
// prevent to run new update while cancel the previous update
|
||||||
|
e.waitForCloseTheDelayedUpdate()
|
||||||
|
|
||||||
|
e.log.Debugf("configure up WireGuard and wait for handshake")
|
||||||
|
var ctx context.Context
|
||||||
|
ctx, e.cancelFunc = context.WithCancel(context.Background())
|
||||||
|
e.updateWg.Add(1)
|
||||||
|
go e.scheduleDelayedUpdate(ctx, addr, presharedKey)
|
||||||
|
|
||||||
|
if err := e.updateWireGuardPeer(nil, presharedKey); err != nil {
|
||||||
|
e.waitForCloseTheDelayedUpdate()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *EndpointUpdater) waitForCloseTheDelayedUpdate() {
|
func (e *EndpointUpdater) waitForCloseTheDelayedUpdate() {
|
||||||
if e.cancelFunc == nil {
|
if e.cancelFunc == nil {
|
||||||
return
|
return
|
||||||
@@ -101,3 +132,9 @@ func (e *EndpointUpdater) updateWireGuardPeer(endpoint *net.UDPAddr, presharedKe
|
|||||||
presharedKey,
|
presharedKey,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wgConfigWorkaround is a workaround for the issue with WireGuard configuration update
|
||||||
|
// When update a peer configuration in near to each other time, the second update can be ignored by WireGuard
|
||||||
|
func wgConfigWorkaround() {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ type WGWatcher struct {
|
|||||||
|
|
||||||
enabled bool
|
enabled bool
|
||||||
muEnabled sync.RWMutex
|
muEnabled sync.RWMutex
|
||||||
|
|
||||||
|
resetCh chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWGWatcher(log *log.Entry, wgIfaceStater WGInterfaceStater, peerKey string, stateDump *stateDump) *WGWatcher {
|
func NewWGWatcher(log *log.Entry, wgIfaceStater WGInterfaceStater, peerKey string, stateDump *stateDump) *WGWatcher {
|
||||||
@@ -40,6 +42,7 @@ func NewWGWatcher(log *log.Entry, wgIfaceStater WGInterfaceStater, peerKey strin
|
|||||||
wgIfaceStater: wgIfaceStater,
|
wgIfaceStater: wgIfaceStater,
|
||||||
peerKey: peerKey,
|
peerKey: peerKey,
|
||||||
stateDump: stateDump,
|
stateDump: stateDump,
|
||||||
|
resetCh: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +79,15 @@ func (w *WGWatcher) IsEnabled() bool {
|
|||||||
return w.enabled
|
return w.enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset signals the watcher that the WireGuard peer has been reset and a new
|
||||||
|
// handshake is expected. This restarts the handshake timeout from scratch.
|
||||||
|
func (w *WGWatcher) Reset() {
|
||||||
|
select {
|
||||||
|
case w.resetCh <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// wgStateCheck help to check the state of the WireGuard handshake and relay connection
|
// wgStateCheck help to check the state of the WireGuard handshake and relay connection
|
||||||
func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, onDisconnectedFn func(), enabledTime time.Time, initialHandshake time.Time) {
|
func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, onDisconnectedFn func(), enabledTime time.Time, initialHandshake time.Time) {
|
||||||
w.log.Infof("WireGuard watcher started")
|
w.log.Infof("WireGuard watcher started")
|
||||||
@@ -105,6 +117,12 @@ func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, onDisconnectedFn
|
|||||||
w.stateDump.WGcheckSuccess()
|
w.stateDump.WGcheckSuccess()
|
||||||
|
|
||||||
w.log.Debugf("WireGuard watcher reset timer: %v", resetTime)
|
w.log.Debugf("WireGuard watcher reset timer: %v", resetTime)
|
||||||
|
case <-w.resetCh:
|
||||||
|
w.log.Infof("WireGuard watcher received peer reset, restarting handshake timeout")
|
||||||
|
lastHandshake = time.Time{}
|
||||||
|
enabledTime = time.Now()
|
||||||
|
timer.Stop()
|
||||||
|
timer.Reset(wgHandshakeOvertime)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
w.log.Infof("WireGuard watcher stopped")
|
w.log.Infof("WireGuard watcher stopped")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ type WorkerICE struct {
|
|||||||
// increase by one when disconnecting the agent
|
// increase by one when disconnecting the agent
|
||||||
// with it the remote peer can discard the already deprecated offer/answer
|
// with it the remote peer can discard the already deprecated offer/answer
|
||||||
// Without it the remote peer may recreate a workable ICE connection
|
// Without it the remote peer may recreate a workable ICE connection
|
||||||
sessionID ICESessionID
|
sessionID ICESessionID
|
||||||
muxAgent sync.Mutex
|
remoteSessionChanged bool
|
||||||
|
muxAgent sync.Mutex
|
||||||
|
|
||||||
localUfrag string
|
localUfrag string
|
||||||
localPwd string
|
localPwd string
|
||||||
@@ -106,6 +107,7 @@ func (w *WorkerICE) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.log.Debugf("agent already exists, recreate the connection")
|
w.log.Debugf("agent already exists, recreate the connection")
|
||||||
|
w.remoteSessionChanged = true
|
||||||
w.agentDialerCancel()
|
w.agentDialerCancel()
|
||||||
if w.agent != nil {
|
if w.agent != nil {
|
||||||
if err := w.agent.Close(); err != nil {
|
if err := w.agent.Close(); err != nil {
|
||||||
@@ -306,13 +308,17 @@ func (w *WorkerICE) connect(ctx context.Context, agent *icemaker.ThreadSafeAgent
|
|||||||
w.conn.onICEConnectionIsReady(selectedPriority(pair), ci)
|
w.conn.onICEConnectionIsReady(selectedPriority(pair), ci)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerICE) closeAgent(agent *icemaker.ThreadSafeAgent, cancel context.CancelFunc) {
|
func (w *WorkerICE) closeAgent(agent *icemaker.ThreadSafeAgent, cancel context.CancelFunc) bool {
|
||||||
cancel()
|
cancel()
|
||||||
if err := agent.Close(); err != nil {
|
if err := agent.Close(); err != nil {
|
||||||
w.log.Warnf("failed to close ICE agent: %s", err)
|
w.log.Warnf("failed to close ICE agent: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.muxAgent.Lock()
|
w.muxAgent.Lock()
|
||||||
|
defer w.muxAgent.Unlock()
|
||||||
|
|
||||||
|
sessionChanged := w.remoteSessionChanged
|
||||||
|
w.remoteSessionChanged = false
|
||||||
|
|
||||||
if w.agent == agent {
|
if w.agent == agent {
|
||||||
// consider to remove from here and move to the OnNewOffer
|
// consider to remove from here and move to the OnNewOffer
|
||||||
@@ -325,7 +331,7 @@ func (w *WorkerICE) closeAgent(agent *icemaker.ThreadSafeAgent, cancel context.C
|
|||||||
w.agentConnecting = false
|
w.agentConnecting = false
|
||||||
w.remoteSessionID = ""
|
w.remoteSessionID = ""
|
||||||
}
|
}
|
||||||
w.muxAgent.Unlock()
|
return sessionChanged
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerICE) punchRemoteWGPort(pair *ice.CandidatePair, remoteWgPort int) {
|
func (w *WorkerICE) punchRemoteWGPort(pair *ice.CandidatePair, remoteWgPort int) {
|
||||||
@@ -426,11 +432,11 @@ func (w *WorkerICE) onConnectionStateChange(agent *icemaker.ThreadSafeAgent, dia
|
|||||||
// ice.ConnectionStateClosed happens when we recreate the agent. For the P2P to TURN switch important to
|
// ice.ConnectionStateClosed happens when we recreate the agent. For the P2P to TURN switch important to
|
||||||
// notify the conn.onICEStateDisconnected changes to update the current used priority
|
// notify the conn.onICEStateDisconnected changes to update the current used priority
|
||||||
|
|
||||||
w.closeAgent(agent, dialerCancel)
|
sessionChanged := w.closeAgent(agent, dialerCancel)
|
||||||
|
|
||||||
if w.lastKnownState == ice.ConnectionStateConnected {
|
if w.lastKnownState == ice.ConnectionStateConnected {
|
||||||
w.lastKnownState = ice.ConnectionStateDisconnected
|
w.lastKnownState = ice.ConnectionStateDisconnected
|
||||||
w.conn.onICEStateDisconnected()
|
w.conn.onICEStateDisconnected(sessionChanged)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ func getConfigDirForUser(username string) (string, error) {
|
|||||||
|
|
||||||
configDir := filepath.Join(DefaultConfigPathDir, username)
|
configDir := filepath.Join(DefaultConfigPathDir, username)
|
||||||
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(configDir, 0600); err != nil {
|
if err := os.MkdirAll(configDir, 0700); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,9 +206,15 @@ func getConfigDirForUser(username string) (string, error) {
|
|||||||
return configDir, nil
|
return configDir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileExists(path string) bool {
|
func fileExists(path string) (bool, error) {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
return !os.IsNotExist(err)
|
if err == nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// createNewConfig creates a new config generating a new Wireguard key and saving to file
|
// createNewConfig creates a new config generating a new Wireguard key and saving to file
|
||||||
@@ -635,7 +641,11 @@ func isPreSharedKeyHidden(preSharedKey *string) bool {
|
|||||||
|
|
||||||
// UpdateConfig update existing configuration according to input configuration and return with the configuration
|
// UpdateConfig update existing configuration according to input configuration and return with the configuration
|
||||||
func UpdateConfig(input ConfigInput) (*Config, error) {
|
func UpdateConfig(input ConfigInput) (*Config, error) {
|
||||||
if !fileExists(input.ConfigPath) {
|
configExists, err := fileExists(input.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check if config file exists: %w", err)
|
||||||
|
}
|
||||||
|
if !configExists {
|
||||||
return nil, fmt.Errorf("config file %s does not exist", input.ConfigPath)
|
return nil, fmt.Errorf("config file %s does not exist", input.ConfigPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,7 +654,11 @@ func UpdateConfig(input ConfigInput) (*Config, error) {
|
|||||||
|
|
||||||
// UpdateOrCreateConfig reads existing config or generates a new one
|
// UpdateOrCreateConfig reads existing config or generates a new one
|
||||||
func UpdateOrCreateConfig(input ConfigInput) (*Config, error) {
|
func UpdateOrCreateConfig(input ConfigInput) (*Config, error) {
|
||||||
if !fileExists(input.ConfigPath) {
|
configExists, err := fileExists(input.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check if config file exists: %w", err)
|
||||||
|
}
|
||||||
|
if !configExists {
|
||||||
log.Infof("generating new config %s", input.ConfigPath)
|
log.Infof("generating new config %s", input.ConfigPath)
|
||||||
cfg, err := createNewConfig(input)
|
cfg, err := createNewConfig(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -657,7 +671,7 @@ func UpdateOrCreateConfig(input ConfigInput) (*Config, error) {
|
|||||||
if isPreSharedKeyHidden(input.PreSharedKey) {
|
if isPreSharedKeyHidden(input.PreSharedKey) {
|
||||||
input.PreSharedKey = nil
|
input.PreSharedKey = nil
|
||||||
}
|
}
|
||||||
err := util.EnforcePermission(input.ConfigPath)
|
err = util.EnforcePermission(input.ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to enforce permission on config dir: %v", err)
|
log.Errorf("failed to enforce permission on config dir: %v", err)
|
||||||
}
|
}
|
||||||
@@ -784,7 +798,12 @@ func ReadConfig(configPath string) (*Config, error) {
|
|||||||
|
|
||||||
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
|
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
|
||||||
func readConfig(configPath string, createIfMissing bool) (*Config, error) {
|
func readConfig(configPath string, createIfMissing bool) (*Config, error) {
|
||||||
if fileExists(configPath) {
|
configExists, err := fileExists(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check if config file exists: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if configExists {
|
||||||
err := util.EnforcePermission(configPath)
|
err := util.EnforcePermission(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to enforce permission on config dir: %v", err)
|
log.Errorf("failed to enforce permission on config dir: %v", err)
|
||||||
@@ -831,7 +850,11 @@ func DirectWriteOutConfig(path string, config *Config) error {
|
|||||||
// DirectUpdateOrCreateConfig is like UpdateOrCreateConfig but uses direct (non-atomic) writes.
|
// DirectUpdateOrCreateConfig is like UpdateOrCreateConfig but uses direct (non-atomic) writes.
|
||||||
// Use this on platforms where atomic writes are blocked (e.g., tvOS sandbox).
|
// Use this on platforms where atomic writes are blocked (e.g., tvOS sandbox).
|
||||||
func DirectUpdateOrCreateConfig(input ConfigInput) (*Config, error) {
|
func DirectUpdateOrCreateConfig(input ConfigInput) (*Config, error) {
|
||||||
if !fileExists(input.ConfigPath) {
|
configExists, err := fileExists(input.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check if config file exists: %w", err)
|
||||||
|
}
|
||||||
|
if !configExists {
|
||||||
log.Infof("generating new config %s", input.ConfigPath)
|
log.Infof("generating new config %s", input.ConfigPath)
|
||||||
cfg, err := createNewConfig(input)
|
cfg, err := createNewConfig(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -256,7 +256,11 @@ func (s *ServiceManager) AddProfile(profileName, username string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
profPath := filepath.Join(configDir, profileName+".json")
|
profPath := filepath.Join(configDir, profileName+".json")
|
||||||
if fileExists(profPath) {
|
profileExists, err := fileExists(profPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check if profile exists: %w", err)
|
||||||
|
}
|
||||||
|
if profileExists {
|
||||||
return ErrProfileAlreadyExists
|
return ErrProfileAlreadyExists
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +289,11 @@ func (s *ServiceManager) RemoveProfile(profileName, username string) error {
|
|||||||
return fmt.Errorf("cannot remove profile with reserved name: %s", defaultProfileName)
|
return fmt.Errorf("cannot remove profile with reserved name: %s", defaultProfileName)
|
||||||
}
|
}
|
||||||
profPath := filepath.Join(configDir, profileName+".json")
|
profPath := filepath.Join(configDir, profileName+".json")
|
||||||
if !fileExists(profPath) {
|
profileExists, err := fileExists(profPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check if profile exists: %w", err)
|
||||||
|
}
|
||||||
|
if !profileExists {
|
||||||
return ErrProfileNotFound
|
return ErrProfileNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ func (pm *ProfileManager) GetProfileState(profileName string) (*ProfileState, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
stateFile := filepath.Join(configDir, profileName+".state.json")
|
stateFile := filepath.Join(configDir, profileName+".state.json")
|
||||||
if !fileExists(stateFile) {
|
stateFileExists, err := fileExists(stateFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check if profile state file exists: %w", err)
|
||||||
|
}
|
||||||
|
if !stateFileExists {
|
||||||
return nil, errors.New("profile state file does not exist")
|
return nil, errors.New("profile state file does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -263,8 +263,14 @@ func (w *Watcher) watchPeerStatusChanges(ctx context.Context, peerKey string, pe
|
|||||||
case <-closer:
|
case <-closer:
|
||||||
return
|
return
|
||||||
case routerStates := <-subscription.Events():
|
case routerStates := <-subscription.Events():
|
||||||
peerStateUpdate <- routerStates
|
select {
|
||||||
log.Debugf("triggered route state update for Peer: %s", peerKey)
|
case peerStateUpdate <- routerStates:
|
||||||
|
log.Debugf("triggered route state update for Peer: %s", peerKey)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-closer:
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,6 +351,11 @@ func (d *DnsInterceptor) writeMsg(w dns.ResponseWriter, r *dns.Msg, logger *log.
|
|||||||
logger.Errorf("failed to update domain prefixes: %v", err)
|
logger.Errorf("failed to update domain prefixes: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow time for route changes to be applied before sending
|
||||||
|
// the DNS response (relevant on iOS where setTunnelNetworkSettings
|
||||||
|
// is asynchronous).
|
||||||
|
waitForRouteSettlement(logger)
|
||||||
|
|
||||||
d.replaceIPsInDNSResponse(r, newPrefixes, logger)
|
d.replaceIPsInDNSResponse(r, newPrefixes, logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
client/internal/routemanager/dnsinterceptor/handler_ios.go
Normal file
20
client/internal/routemanager/dnsinterceptor/handler_ios.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//go:build ios
|
||||||
|
|
||||||
|
package dnsinterceptor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const routeSettleDelay = 500 * time.Millisecond
|
||||||
|
|
||||||
|
// waitForRouteSettlement introduces a short delay on iOS to allow
|
||||||
|
// setTunnelNetworkSettings to apply route changes before the DNS
|
||||||
|
// response reaches the application. Without this, the first request
|
||||||
|
// to a newly resolved domain may bypass the tunnel.
|
||||||
|
func waitForRouteSettlement(logger *log.Entry) {
|
||||||
|
logger.Tracef("waiting %v for iOS route settlement", routeSettleDelay)
|
||||||
|
time.Sleep(routeSettleDelay)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build !ios
|
||||||
|
|
||||||
|
package dnsinterceptor
|
||||||
|
|
||||||
|
import log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
func waitForRouteSettlement(_ *log.Entry) {
|
||||||
|
// No-op on non-iOS platforms: route changes are applied synchronously by
|
||||||
|
// the kernel, so no settlement delay is needed before the DNS response
|
||||||
|
// reaches the application. The delay is only required on iOS where
|
||||||
|
// setTunnelNetworkSettings applies routes asynchronously.
|
||||||
|
}
|
||||||
@@ -346,6 +346,23 @@ func (m *DefaultManager) updateSystemRoutes(newRoutes route.HAMap) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
|
|
||||||
|
// Begin batch mode to avoid calling applyHostConfig() after each DNS handler operation
|
||||||
|
batchStarted := false
|
||||||
|
if m.dnsServer != nil {
|
||||||
|
m.dnsServer.BeginBatch()
|
||||||
|
batchStarted = true
|
||||||
|
defer func() {
|
||||||
|
if merr != nil {
|
||||||
|
// On error, cancel batch to discard partial DNS state
|
||||||
|
m.dnsServer.CancelBatch()
|
||||||
|
} else {
|
||||||
|
// On success, apply accumulated DNS changes
|
||||||
|
m.dnsServer.EndBatch()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
for id, handler := range toRemove {
|
for id, handler := range toRemove {
|
||||||
if err := handler.RemoveRoute(); err != nil {
|
if err := handler.RemoveRoute(); err != nil {
|
||||||
merr = multierror.Append(merr, fmt.Errorf("remove route %s: %w", handler.String(), err))
|
merr = multierror.Append(merr, fmt.Errorf("remove route %s: %w", handler.String(), err))
|
||||||
@@ -376,6 +393,7 @@ func (m *DefaultManager) updateSystemRoutes(newRoutes route.HAMap) error {
|
|||||||
m.activeRoutes[id] = handler
|
m.activeRoutes[id] = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = batchStarted // Mark as used
|
||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
80
client/internal/sleep/handler/handler.go
Normal file
80
client/internal/sleep/handler/handler.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Agent interface {
|
||||||
|
Up(ctx context.Context) error
|
||||||
|
Down(ctx context.Context) error
|
||||||
|
Status() (internal.StatusType, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SleepHandler struct {
|
||||||
|
agent Agent
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
// sleepTriggeredDown indicates whether the sleep handler triggered the last client down, to avoid unnecessary up on wake
|
||||||
|
sleepTriggeredDown bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(agent Agent) *SleepHandler {
|
||||||
|
return &SleepHandler{
|
||||||
|
agent: agent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SleepHandler) HandleWakeUp(ctx context.Context) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if !s.sleepTriggeredDown {
|
||||||
|
log.Info("skipping up because wasn't sleep down")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// avoid other wakeup runs if sleep didn't make the computer sleep
|
||||||
|
s.sleepTriggeredDown = false
|
||||||
|
|
||||||
|
log.Info("running up after wake up")
|
||||||
|
err := s.agent.Up(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("running up failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("running up command executed successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SleepHandler) HandleSleep(ctx context.Context) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
status, err := s.agent.Status()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if status != internal.StatusConnecting && status != internal.StatusConnected {
|
||||||
|
log.Infof("skipping setting the agent down because status is %s", status)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("running down after system started sleeping")
|
||||||
|
|
||||||
|
if err = s.agent.Down(ctx); err != nil {
|
||||||
|
log.Errorf("running down failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.sleepTriggeredDown = true
|
||||||
|
|
||||||
|
log.Info("running down executed successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
153
client/internal/sleep/handler/handler_test.go
Normal file
153
client/internal/sleep/handler/handler_test.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockAgent struct {
|
||||||
|
upErr error
|
||||||
|
downErr error
|
||||||
|
statusErr error
|
||||||
|
status internal.StatusType
|
||||||
|
upCalls int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAgent) Up(_ context.Context) error {
|
||||||
|
m.upCalls++
|
||||||
|
return m.upErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAgent) Down(_ context.Context) error {
|
||||||
|
return m.downErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAgent) Status() (internal.StatusType, error) {
|
||||||
|
return m.status, m.statusErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHandler(status internal.StatusType) (*SleepHandler, *mockAgent) {
|
||||||
|
agent := &mockAgent{status: status}
|
||||||
|
return New(agent), agent
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleWakeUp_SkipsWhenFlagFalse(t *testing.T) {
|
||||||
|
h, agent := newHandler(internal.StatusIdle)
|
||||||
|
|
||||||
|
err := h.HandleWakeUp(context.Background())
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, agent.upCalls, "Up should not be called when flag is false")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleWakeUp_ResetsFlagBeforeUp(t *testing.T) {
|
||||||
|
h, _ := newHandler(internal.StatusIdle)
|
||||||
|
h.sleepTriggeredDown = true
|
||||||
|
|
||||||
|
// Even if Up fails, flag should be reset
|
||||||
|
_ = h.HandleWakeUp(context.Background())
|
||||||
|
|
||||||
|
assert.False(t, h.sleepTriggeredDown, "flag must be reset before calling Up")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleWakeUp_CallsUpWhenFlagSet(t *testing.T) {
|
||||||
|
h, agent := newHandler(internal.StatusIdle)
|
||||||
|
h.sleepTriggeredDown = true
|
||||||
|
|
||||||
|
err := h.HandleWakeUp(context.Background())
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, agent.upCalls)
|
||||||
|
assert.False(t, h.sleepTriggeredDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleWakeUp_ReturnsErrorFromUp(t *testing.T) {
|
||||||
|
h, agent := newHandler(internal.StatusIdle)
|
||||||
|
h.sleepTriggeredDown = true
|
||||||
|
agent.upErr = errors.New("up failed")
|
||||||
|
|
||||||
|
err := h.HandleWakeUp(context.Background())
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, agent.upErr)
|
||||||
|
assert.False(t, h.sleepTriggeredDown, "flag should still be reset even when Up fails")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleWakeUp_SecondCallIsNoOp(t *testing.T) {
|
||||||
|
h, agent := newHandler(internal.StatusIdle)
|
||||||
|
h.sleepTriggeredDown = true
|
||||||
|
|
||||||
|
_ = h.HandleWakeUp(context.Background())
|
||||||
|
err := h.HandleWakeUp(context.Background())
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, agent.upCalls, "second wakeup should be no-op")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleSleep_SkipsForNonActiveStates(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status internal.StatusType
|
||||||
|
}{
|
||||||
|
{"Idle", internal.StatusIdle},
|
||||||
|
{"NeedsLogin", internal.StatusNeedsLogin},
|
||||||
|
{"LoginFailed", internal.StatusLoginFailed},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h, _ := newHandler(tt.status)
|
||||||
|
|
||||||
|
err := h.HandleSleep(context.Background())
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, h.sleepTriggeredDown)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleSleep_ProceedsForActiveStates(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status internal.StatusType
|
||||||
|
}{
|
||||||
|
{"Connecting", internal.StatusConnecting},
|
||||||
|
{"Connected", internal.StatusConnected},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h, _ := newHandler(tt.status)
|
||||||
|
|
||||||
|
err := h.HandleSleep(context.Background())
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, h.sleepTriggeredDown)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleSleep_ReturnsErrorFromStatus(t *testing.T) {
|
||||||
|
agent := &mockAgent{statusErr: errors.New("status error")}
|
||||||
|
h := New(agent)
|
||||||
|
|
||||||
|
err := h.HandleSleep(context.Background())
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, agent.statusErr)
|
||||||
|
assert.False(t, h.sleepTriggeredDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleSleep_ReturnsErrorFromDown(t *testing.T) {
|
||||||
|
agent := &mockAgent{status: internal.StatusConnected, downErr: errors.New("down failed")}
|
||||||
|
h := New(agent)
|
||||||
|
|
||||||
|
err := h.HandleSleep(context.Background())
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, agent.downErr)
|
||||||
|
assert.False(t, h.sleepTriggeredDown, "flag should not be set when Down fails")
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
package NetBirdSDK
|
package NetBirdSDK
|
||||||
|
|
||||||
import "github.com/netbirdio/netbird/client/internal/peer"
|
import (
|
||||||
|
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
)
|
||||||
|
|
||||||
// EnvList is an exported struct to be bound by gomobile
|
// EnvList is an exported struct to be bound by gomobile
|
||||||
type EnvList struct {
|
type EnvList struct {
|
||||||
@@ -32,3 +35,13 @@ func (el *EnvList) AllItems() map[string]string {
|
|||||||
func GetEnvKeyNBForceRelay() string {
|
func GetEnvKeyNBForceRelay() string {
|
||||||
return peer.EnvKeyNBForceRelay
|
return peer.EnvKeyNBForceRelay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEnvKeyNBLazyConn Exports the environment variable for the iOS client
|
||||||
|
func GetEnvKeyNBLazyConn() string {
|
||||||
|
return lazyconn.EnvEnableLazyConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnvKeyNBInactivityThreshold Exports the environment variable for the iOS client
|
||||||
|
func GetEnvKeyNBInactivityThreshold() string {
|
||||||
|
return lazyconn.EnvInactivityThreshold
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.6
|
// protoc-gen-go v1.36.6
|
||||||
// protoc v6.32.1
|
// protoc v6.33.3
|
||||||
// source: daemon.proto
|
// source: daemon.proto
|
||||||
|
|
||||||
package proto
|
package proto
|
||||||
@@ -88,6 +88,58 @@ func (LogLevel) EnumDescriptor() ([]byte, []int) {
|
|||||||
return file_daemon_proto_rawDescGZIP(), []int{0}
|
return file_daemon_proto_rawDescGZIP(), []int{0}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExposeProtocol int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
ExposeProtocol_EXPOSE_HTTP ExposeProtocol = 0
|
||||||
|
ExposeProtocol_EXPOSE_HTTPS ExposeProtocol = 1
|
||||||
|
ExposeProtocol_EXPOSE_TCP ExposeProtocol = 2
|
||||||
|
ExposeProtocol_EXPOSE_UDP ExposeProtocol = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enum value maps for ExposeProtocol.
|
||||||
|
var (
|
||||||
|
ExposeProtocol_name = map[int32]string{
|
||||||
|
0: "EXPOSE_HTTP",
|
||||||
|
1: "EXPOSE_HTTPS",
|
||||||
|
2: "EXPOSE_TCP",
|
||||||
|
3: "EXPOSE_UDP",
|
||||||
|
}
|
||||||
|
ExposeProtocol_value = map[string]int32{
|
||||||
|
"EXPOSE_HTTP": 0,
|
||||||
|
"EXPOSE_HTTPS": 1,
|
||||||
|
"EXPOSE_TCP": 2,
|
||||||
|
"EXPOSE_UDP": 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x ExposeProtocol) Enum() *ExposeProtocol {
|
||||||
|
p := new(ExposeProtocol)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ExposeProtocol) String() string {
|
||||||
|
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ExposeProtocol) Descriptor() protoreflect.EnumDescriptor {
|
||||||
|
return file_daemon_proto_enumTypes[1].Descriptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ExposeProtocol) Type() protoreflect.EnumType {
|
||||||
|
return &file_daemon_proto_enumTypes[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ExposeProtocol) Number() protoreflect.EnumNumber {
|
||||||
|
return protoreflect.EnumNumber(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ExposeProtocol.Descriptor instead.
|
||||||
|
func (ExposeProtocol) EnumDescriptor() ([]byte, []int) {
|
||||||
|
return file_daemon_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
// avoid collision with loglevel enum
|
// avoid collision with loglevel enum
|
||||||
type OSLifecycleRequest_CycleType int32
|
type OSLifecycleRequest_CycleType int32
|
||||||
|
|
||||||
@@ -122,11 +174,11 @@ func (x OSLifecycleRequest_CycleType) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (OSLifecycleRequest_CycleType) Descriptor() protoreflect.EnumDescriptor {
|
func (OSLifecycleRequest_CycleType) Descriptor() protoreflect.EnumDescriptor {
|
||||||
return file_daemon_proto_enumTypes[1].Descriptor()
|
return file_daemon_proto_enumTypes[2].Descriptor()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (OSLifecycleRequest_CycleType) Type() protoreflect.EnumType {
|
func (OSLifecycleRequest_CycleType) Type() protoreflect.EnumType {
|
||||||
return &file_daemon_proto_enumTypes[1]
|
return &file_daemon_proto_enumTypes[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x OSLifecycleRequest_CycleType) Number() protoreflect.EnumNumber {
|
func (x OSLifecycleRequest_CycleType) Number() protoreflect.EnumNumber {
|
||||||
@@ -174,11 +226,11 @@ func (x SystemEvent_Severity) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (SystemEvent_Severity) Descriptor() protoreflect.EnumDescriptor {
|
func (SystemEvent_Severity) Descriptor() protoreflect.EnumDescriptor {
|
||||||
return file_daemon_proto_enumTypes[2].Descriptor()
|
return file_daemon_proto_enumTypes[3].Descriptor()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (SystemEvent_Severity) Type() protoreflect.EnumType {
|
func (SystemEvent_Severity) Type() protoreflect.EnumType {
|
||||||
return &file_daemon_proto_enumTypes[2]
|
return &file_daemon_proto_enumTypes[3]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x SystemEvent_Severity) Number() protoreflect.EnumNumber {
|
func (x SystemEvent_Severity) Number() protoreflect.EnumNumber {
|
||||||
@@ -229,11 +281,11 @@ func (x SystemEvent_Category) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (SystemEvent_Category) Descriptor() protoreflect.EnumDescriptor {
|
func (SystemEvent_Category) Descriptor() protoreflect.EnumDescriptor {
|
||||||
return file_daemon_proto_enumTypes[3].Descriptor()
|
return file_daemon_proto_enumTypes[4].Descriptor()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (SystemEvent_Category) Type() protoreflect.EnumType {
|
func (SystemEvent_Category) Type() protoreflect.EnumType {
|
||||||
return &file_daemon_proto_enumTypes[3]
|
return &file_daemon_proto_enumTypes[4]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x SystemEvent_Category) Number() protoreflect.EnumNumber {
|
func (x SystemEvent_Category) Number() protoreflect.EnumNumber {
|
||||||
@@ -5600,6 +5652,224 @@ func (x *InstallerResultResponse) GetErrorMsg() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExposeServiceRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Port uint32 `protobuf:"varint,1,opt,name=port,proto3" json:"port,omitempty"`
|
||||||
|
Protocol ExposeProtocol `protobuf:"varint,2,opt,name=protocol,proto3,enum=daemon.ExposeProtocol" json:"protocol,omitempty"`
|
||||||
|
Pin string `protobuf:"bytes,3,opt,name=pin,proto3" json:"pin,omitempty"`
|
||||||
|
Password string `protobuf:"bytes,4,opt,name=password,proto3" json:"password,omitempty"`
|
||||||
|
UserGroups []string `protobuf:"bytes,5,rep,name=user_groups,json=userGroups,proto3" json:"user_groups,omitempty"`
|
||||||
|
Domain string `protobuf:"bytes,6,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||||
|
NamePrefix string `protobuf:"bytes,7,opt,name=name_prefix,json=namePrefix,proto3" json:"name_prefix,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceRequest) Reset() {
|
||||||
|
*x = ExposeServiceRequest{}
|
||||||
|
mi := &file_daemon_proto_msgTypes[85]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ExposeServiceRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_daemon_proto_msgTypes[85]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ExposeServiceRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_daemon_proto_rawDescGZIP(), []int{85}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceRequest) GetPort() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Port
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceRequest) GetProtocol() ExposeProtocol {
|
||||||
|
if x != nil {
|
||||||
|
return x.Protocol
|
||||||
|
}
|
||||||
|
return ExposeProtocol_EXPOSE_HTTP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceRequest) GetPin() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Pin
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceRequest) GetPassword() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Password
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceRequest) GetUserGroups() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.UserGroups
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceRequest) GetDomain() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Domain
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceRequest) GetNamePrefix() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.NamePrefix
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExposeServiceEvent struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
// Types that are valid to be assigned to Event:
|
||||||
|
//
|
||||||
|
// *ExposeServiceEvent_Ready
|
||||||
|
Event isExposeServiceEvent_Event `protobuf_oneof:"event"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceEvent) Reset() {
|
||||||
|
*x = ExposeServiceEvent{}
|
||||||
|
mi := &file_daemon_proto_msgTypes[86]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceEvent) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ExposeServiceEvent) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_daemon_proto_msgTypes[86]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ExposeServiceEvent.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ExposeServiceEvent) Descriptor() ([]byte, []int) {
|
||||||
|
return file_daemon_proto_rawDescGZIP(), []int{86}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceEvent) GetEvent() isExposeServiceEvent_Event {
|
||||||
|
if x != nil {
|
||||||
|
return x.Event
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceEvent) GetReady() *ExposeServiceReady {
|
||||||
|
if x != nil {
|
||||||
|
if x, ok := x.Event.(*ExposeServiceEvent_Ready); ok {
|
||||||
|
return x.Ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type isExposeServiceEvent_Event interface {
|
||||||
|
isExposeServiceEvent_Event()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExposeServiceEvent_Ready struct {
|
||||||
|
Ready *ExposeServiceReady `protobuf:"bytes,1,opt,name=ready,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ExposeServiceEvent_Ready) isExposeServiceEvent_Event() {}
|
||||||
|
|
||||||
|
type ExposeServiceReady struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"`
|
||||||
|
ServiceUrl string `protobuf:"bytes,2,opt,name=service_url,json=serviceUrl,proto3" json:"service_url,omitempty"`
|
||||||
|
Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceReady) Reset() {
|
||||||
|
*x = ExposeServiceReady{}
|
||||||
|
mi := &file_daemon_proto_msgTypes[87]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceReady) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ExposeServiceReady) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_daemon_proto_msgTypes[87]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ExposeServiceReady.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ExposeServiceReady) Descriptor() ([]byte, []int) {
|
||||||
|
return file_daemon_proto_rawDescGZIP(), []int{87}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceReady) GetServiceName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ServiceName
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceReady) GetServiceUrl() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ServiceUrl
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExposeServiceReady) GetDomain() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Domain
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type PortInfo_Range struct {
|
type PortInfo_Range struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"`
|
Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"`
|
||||||
@@ -5610,7 +5880,7 @@ type PortInfo_Range struct {
|
|||||||
|
|
||||||
func (x *PortInfo_Range) Reset() {
|
func (x *PortInfo_Range) Reset() {
|
||||||
*x = PortInfo_Range{}
|
*x = PortInfo_Range{}
|
||||||
mi := &file_daemon_proto_msgTypes[86]
|
mi := &file_daemon_proto_msgTypes[89]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -5622,7 +5892,7 @@ func (x *PortInfo_Range) String() string {
|
|||||||
func (*PortInfo_Range) ProtoMessage() {}
|
func (*PortInfo_Range) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *PortInfo_Range) ProtoReflect() protoreflect.Message {
|
func (x *PortInfo_Range) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_daemon_proto_msgTypes[86]
|
mi := &file_daemon_proto_msgTypes[89]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -6149,7 +6419,25 @@ const file_daemon_proto_rawDesc = "" +
|
|||||||
"\x16InstallerResultRequest\"O\n" +
|
"\x16InstallerResultRequest\"O\n" +
|
||||||
"\x17InstallerResultResponse\x12\x18\n" +
|
"\x17InstallerResultResponse\x12\x18\n" +
|
||||||
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1a\n" +
|
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1a\n" +
|
||||||
"\berrorMsg\x18\x02 \x01(\tR\berrorMsg*b\n" +
|
"\berrorMsg\x18\x02 \x01(\tR\berrorMsg\"\xe6\x01\n" +
|
||||||
|
"\x14ExposeServiceRequest\x12\x12\n" +
|
||||||
|
"\x04port\x18\x01 \x01(\rR\x04port\x122\n" +
|
||||||
|
"\bprotocol\x18\x02 \x01(\x0e2\x16.daemon.ExposeProtocolR\bprotocol\x12\x10\n" +
|
||||||
|
"\x03pin\x18\x03 \x01(\tR\x03pin\x12\x1a\n" +
|
||||||
|
"\bpassword\x18\x04 \x01(\tR\bpassword\x12\x1f\n" +
|
||||||
|
"\vuser_groups\x18\x05 \x03(\tR\n" +
|
||||||
|
"userGroups\x12\x16\n" +
|
||||||
|
"\x06domain\x18\x06 \x01(\tR\x06domain\x12\x1f\n" +
|
||||||
|
"\vname_prefix\x18\a \x01(\tR\n" +
|
||||||
|
"namePrefix\"Q\n" +
|
||||||
|
"\x12ExposeServiceEvent\x122\n" +
|
||||||
|
"\x05ready\x18\x01 \x01(\v2\x1a.daemon.ExposeServiceReadyH\x00R\x05readyB\a\n" +
|
||||||
|
"\x05event\"p\n" +
|
||||||
|
"\x12ExposeServiceReady\x12!\n" +
|
||||||
|
"\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x1f\n" +
|
||||||
|
"\vservice_url\x18\x02 \x01(\tR\n" +
|
||||||
|
"serviceUrl\x12\x16\n" +
|
||||||
|
"\x06domain\x18\x03 \x01(\tR\x06domain*b\n" +
|
||||||
"\bLogLevel\x12\v\n" +
|
"\bLogLevel\x12\v\n" +
|
||||||
"\aUNKNOWN\x10\x00\x12\t\n" +
|
"\aUNKNOWN\x10\x00\x12\t\n" +
|
||||||
"\x05PANIC\x10\x01\x12\t\n" +
|
"\x05PANIC\x10\x01\x12\t\n" +
|
||||||
@@ -6158,7 +6446,14 @@ const file_daemon_proto_rawDesc = "" +
|
|||||||
"\x04WARN\x10\x04\x12\b\n" +
|
"\x04WARN\x10\x04\x12\b\n" +
|
||||||
"\x04INFO\x10\x05\x12\t\n" +
|
"\x04INFO\x10\x05\x12\t\n" +
|
||||||
"\x05DEBUG\x10\x06\x12\t\n" +
|
"\x05DEBUG\x10\x06\x12\t\n" +
|
||||||
"\x05TRACE\x10\a2\xdd\x14\n" +
|
"\x05TRACE\x10\a*S\n" +
|
||||||
|
"\x0eExposeProtocol\x12\x0f\n" +
|
||||||
|
"\vEXPOSE_HTTP\x10\x00\x12\x10\n" +
|
||||||
|
"\fEXPOSE_HTTPS\x10\x01\x12\x0e\n" +
|
||||||
|
"\n" +
|
||||||
|
"EXPOSE_TCP\x10\x02\x12\x0e\n" +
|
||||||
|
"\n" +
|
||||||
|
"EXPOSE_UDP\x10\x032\xac\x15\n" +
|
||||||
"\rDaemonService\x126\n" +
|
"\rDaemonService\x126\n" +
|
||||||
"\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" +
|
"\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" +
|
||||||
"\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" +
|
"\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" +
|
||||||
@@ -6197,7 +6492,8 @@ const file_daemon_proto_rawDesc = "" +
|
|||||||
"\x0fStartCPUProfile\x12\x1e.daemon.StartCPUProfileRequest\x1a\x1f.daemon.StartCPUProfileResponse\"\x00\x12Q\n" +
|
"\x0fStartCPUProfile\x12\x1e.daemon.StartCPUProfileRequest\x1a\x1f.daemon.StartCPUProfileResponse\"\x00\x12Q\n" +
|
||||||
"\x0eStopCPUProfile\x12\x1d.daemon.StopCPUProfileRequest\x1a\x1e.daemon.StopCPUProfileResponse\"\x00\x12N\n" +
|
"\x0eStopCPUProfile\x12\x1d.daemon.StopCPUProfileRequest\x1a\x1e.daemon.StopCPUProfileResponse\"\x00\x12N\n" +
|
||||||
"\x11NotifyOSLifecycle\x12\x1a.daemon.OSLifecycleRequest\x1a\x1b.daemon.OSLifecycleResponse\"\x00\x12W\n" +
|
"\x11NotifyOSLifecycle\x12\x1a.daemon.OSLifecycleRequest\x1a\x1b.daemon.OSLifecycleResponse\"\x00\x12W\n" +
|
||||||
"\x12GetInstallerResult\x12\x1e.daemon.InstallerResultRequest\x1a\x1f.daemon.InstallerResultResponse\"\x00B\bZ\x06/protob\x06proto3"
|
"\x12GetInstallerResult\x12\x1e.daemon.InstallerResultRequest\x1a\x1f.daemon.InstallerResultResponse\"\x00\x12M\n" +
|
||||||
|
"\rExposeService\x12\x1c.daemon.ExposeServiceRequest\x1a\x1a.daemon.ExposeServiceEvent\"\x000\x01B\bZ\x06/protob\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_daemon_proto_rawDescOnce sync.Once
|
file_daemon_proto_rawDescOnce sync.Once
|
||||||
@@ -6211,214 +6507,222 @@ func file_daemon_proto_rawDescGZIP() []byte {
|
|||||||
return file_daemon_proto_rawDescData
|
return file_daemon_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
|
var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
|
||||||
var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 88)
|
var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 91)
|
||||||
var file_daemon_proto_goTypes = []any{
|
var file_daemon_proto_goTypes = []any{
|
||||||
(LogLevel)(0), // 0: daemon.LogLevel
|
(LogLevel)(0), // 0: daemon.LogLevel
|
||||||
(OSLifecycleRequest_CycleType)(0), // 1: daemon.OSLifecycleRequest.CycleType
|
(ExposeProtocol)(0), // 1: daemon.ExposeProtocol
|
||||||
(SystemEvent_Severity)(0), // 2: daemon.SystemEvent.Severity
|
(OSLifecycleRequest_CycleType)(0), // 2: daemon.OSLifecycleRequest.CycleType
|
||||||
(SystemEvent_Category)(0), // 3: daemon.SystemEvent.Category
|
(SystemEvent_Severity)(0), // 3: daemon.SystemEvent.Severity
|
||||||
(*EmptyRequest)(nil), // 4: daemon.EmptyRequest
|
(SystemEvent_Category)(0), // 4: daemon.SystemEvent.Category
|
||||||
(*OSLifecycleRequest)(nil), // 5: daemon.OSLifecycleRequest
|
(*EmptyRequest)(nil), // 5: daemon.EmptyRequest
|
||||||
(*OSLifecycleResponse)(nil), // 6: daemon.OSLifecycleResponse
|
(*OSLifecycleRequest)(nil), // 6: daemon.OSLifecycleRequest
|
||||||
(*LoginRequest)(nil), // 7: daemon.LoginRequest
|
(*OSLifecycleResponse)(nil), // 7: daemon.OSLifecycleResponse
|
||||||
(*LoginResponse)(nil), // 8: daemon.LoginResponse
|
(*LoginRequest)(nil), // 8: daemon.LoginRequest
|
||||||
(*WaitSSOLoginRequest)(nil), // 9: daemon.WaitSSOLoginRequest
|
(*LoginResponse)(nil), // 9: daemon.LoginResponse
|
||||||
(*WaitSSOLoginResponse)(nil), // 10: daemon.WaitSSOLoginResponse
|
(*WaitSSOLoginRequest)(nil), // 10: daemon.WaitSSOLoginRequest
|
||||||
(*UpRequest)(nil), // 11: daemon.UpRequest
|
(*WaitSSOLoginResponse)(nil), // 11: daemon.WaitSSOLoginResponse
|
||||||
(*UpResponse)(nil), // 12: daemon.UpResponse
|
(*UpRequest)(nil), // 12: daemon.UpRequest
|
||||||
(*StatusRequest)(nil), // 13: daemon.StatusRequest
|
(*UpResponse)(nil), // 13: daemon.UpResponse
|
||||||
(*StatusResponse)(nil), // 14: daemon.StatusResponse
|
(*StatusRequest)(nil), // 14: daemon.StatusRequest
|
||||||
(*DownRequest)(nil), // 15: daemon.DownRequest
|
(*StatusResponse)(nil), // 15: daemon.StatusResponse
|
||||||
(*DownResponse)(nil), // 16: daemon.DownResponse
|
(*DownRequest)(nil), // 16: daemon.DownRequest
|
||||||
(*GetConfigRequest)(nil), // 17: daemon.GetConfigRequest
|
(*DownResponse)(nil), // 17: daemon.DownResponse
|
||||||
(*GetConfigResponse)(nil), // 18: daemon.GetConfigResponse
|
(*GetConfigRequest)(nil), // 18: daemon.GetConfigRequest
|
||||||
(*PeerState)(nil), // 19: daemon.PeerState
|
(*GetConfigResponse)(nil), // 19: daemon.GetConfigResponse
|
||||||
(*LocalPeerState)(nil), // 20: daemon.LocalPeerState
|
(*PeerState)(nil), // 20: daemon.PeerState
|
||||||
(*SignalState)(nil), // 21: daemon.SignalState
|
(*LocalPeerState)(nil), // 21: daemon.LocalPeerState
|
||||||
(*ManagementState)(nil), // 22: daemon.ManagementState
|
(*SignalState)(nil), // 22: daemon.SignalState
|
||||||
(*RelayState)(nil), // 23: daemon.RelayState
|
(*ManagementState)(nil), // 23: daemon.ManagementState
|
||||||
(*NSGroupState)(nil), // 24: daemon.NSGroupState
|
(*RelayState)(nil), // 24: daemon.RelayState
|
||||||
(*SSHSessionInfo)(nil), // 25: daemon.SSHSessionInfo
|
(*NSGroupState)(nil), // 25: daemon.NSGroupState
|
||||||
(*SSHServerState)(nil), // 26: daemon.SSHServerState
|
(*SSHSessionInfo)(nil), // 26: daemon.SSHSessionInfo
|
||||||
(*FullStatus)(nil), // 27: daemon.FullStatus
|
(*SSHServerState)(nil), // 27: daemon.SSHServerState
|
||||||
(*ListNetworksRequest)(nil), // 28: daemon.ListNetworksRequest
|
(*FullStatus)(nil), // 28: daemon.FullStatus
|
||||||
(*ListNetworksResponse)(nil), // 29: daemon.ListNetworksResponse
|
(*ListNetworksRequest)(nil), // 29: daemon.ListNetworksRequest
|
||||||
(*SelectNetworksRequest)(nil), // 30: daemon.SelectNetworksRequest
|
(*ListNetworksResponse)(nil), // 30: daemon.ListNetworksResponse
|
||||||
(*SelectNetworksResponse)(nil), // 31: daemon.SelectNetworksResponse
|
(*SelectNetworksRequest)(nil), // 31: daemon.SelectNetworksRequest
|
||||||
(*IPList)(nil), // 32: daemon.IPList
|
(*SelectNetworksResponse)(nil), // 32: daemon.SelectNetworksResponse
|
||||||
(*Network)(nil), // 33: daemon.Network
|
(*IPList)(nil), // 33: daemon.IPList
|
||||||
(*PortInfo)(nil), // 34: daemon.PortInfo
|
(*Network)(nil), // 34: daemon.Network
|
||||||
(*ForwardingRule)(nil), // 35: daemon.ForwardingRule
|
(*PortInfo)(nil), // 35: daemon.PortInfo
|
||||||
(*ForwardingRulesResponse)(nil), // 36: daemon.ForwardingRulesResponse
|
(*ForwardingRule)(nil), // 36: daemon.ForwardingRule
|
||||||
(*DebugBundleRequest)(nil), // 37: daemon.DebugBundleRequest
|
(*ForwardingRulesResponse)(nil), // 37: daemon.ForwardingRulesResponse
|
||||||
(*DebugBundleResponse)(nil), // 38: daemon.DebugBundleResponse
|
(*DebugBundleRequest)(nil), // 38: daemon.DebugBundleRequest
|
||||||
(*GetLogLevelRequest)(nil), // 39: daemon.GetLogLevelRequest
|
(*DebugBundleResponse)(nil), // 39: daemon.DebugBundleResponse
|
||||||
(*GetLogLevelResponse)(nil), // 40: daemon.GetLogLevelResponse
|
(*GetLogLevelRequest)(nil), // 40: daemon.GetLogLevelRequest
|
||||||
(*SetLogLevelRequest)(nil), // 41: daemon.SetLogLevelRequest
|
(*GetLogLevelResponse)(nil), // 41: daemon.GetLogLevelResponse
|
||||||
(*SetLogLevelResponse)(nil), // 42: daemon.SetLogLevelResponse
|
(*SetLogLevelRequest)(nil), // 42: daemon.SetLogLevelRequest
|
||||||
(*State)(nil), // 43: daemon.State
|
(*SetLogLevelResponse)(nil), // 43: daemon.SetLogLevelResponse
|
||||||
(*ListStatesRequest)(nil), // 44: daemon.ListStatesRequest
|
(*State)(nil), // 44: daemon.State
|
||||||
(*ListStatesResponse)(nil), // 45: daemon.ListStatesResponse
|
(*ListStatesRequest)(nil), // 45: daemon.ListStatesRequest
|
||||||
(*CleanStateRequest)(nil), // 46: daemon.CleanStateRequest
|
(*ListStatesResponse)(nil), // 46: daemon.ListStatesResponse
|
||||||
(*CleanStateResponse)(nil), // 47: daemon.CleanStateResponse
|
(*CleanStateRequest)(nil), // 47: daemon.CleanStateRequest
|
||||||
(*DeleteStateRequest)(nil), // 48: daemon.DeleteStateRequest
|
(*CleanStateResponse)(nil), // 48: daemon.CleanStateResponse
|
||||||
(*DeleteStateResponse)(nil), // 49: daemon.DeleteStateResponse
|
(*DeleteStateRequest)(nil), // 49: daemon.DeleteStateRequest
|
||||||
(*SetSyncResponsePersistenceRequest)(nil), // 50: daemon.SetSyncResponsePersistenceRequest
|
(*DeleteStateResponse)(nil), // 50: daemon.DeleteStateResponse
|
||||||
(*SetSyncResponsePersistenceResponse)(nil), // 51: daemon.SetSyncResponsePersistenceResponse
|
(*SetSyncResponsePersistenceRequest)(nil), // 51: daemon.SetSyncResponsePersistenceRequest
|
||||||
(*TCPFlags)(nil), // 52: daemon.TCPFlags
|
(*SetSyncResponsePersistenceResponse)(nil), // 52: daemon.SetSyncResponsePersistenceResponse
|
||||||
(*TracePacketRequest)(nil), // 53: daemon.TracePacketRequest
|
(*TCPFlags)(nil), // 53: daemon.TCPFlags
|
||||||
(*TraceStage)(nil), // 54: daemon.TraceStage
|
(*TracePacketRequest)(nil), // 54: daemon.TracePacketRequest
|
||||||
(*TracePacketResponse)(nil), // 55: daemon.TracePacketResponse
|
(*TraceStage)(nil), // 55: daemon.TraceStage
|
||||||
(*SubscribeRequest)(nil), // 56: daemon.SubscribeRequest
|
(*TracePacketResponse)(nil), // 56: daemon.TracePacketResponse
|
||||||
(*SystemEvent)(nil), // 57: daemon.SystemEvent
|
(*SubscribeRequest)(nil), // 57: daemon.SubscribeRequest
|
||||||
(*GetEventsRequest)(nil), // 58: daemon.GetEventsRequest
|
(*SystemEvent)(nil), // 58: daemon.SystemEvent
|
||||||
(*GetEventsResponse)(nil), // 59: daemon.GetEventsResponse
|
(*GetEventsRequest)(nil), // 59: daemon.GetEventsRequest
|
||||||
(*SwitchProfileRequest)(nil), // 60: daemon.SwitchProfileRequest
|
(*GetEventsResponse)(nil), // 60: daemon.GetEventsResponse
|
||||||
(*SwitchProfileResponse)(nil), // 61: daemon.SwitchProfileResponse
|
(*SwitchProfileRequest)(nil), // 61: daemon.SwitchProfileRequest
|
||||||
(*SetConfigRequest)(nil), // 62: daemon.SetConfigRequest
|
(*SwitchProfileResponse)(nil), // 62: daemon.SwitchProfileResponse
|
||||||
(*SetConfigResponse)(nil), // 63: daemon.SetConfigResponse
|
(*SetConfigRequest)(nil), // 63: daemon.SetConfigRequest
|
||||||
(*AddProfileRequest)(nil), // 64: daemon.AddProfileRequest
|
(*SetConfigResponse)(nil), // 64: daemon.SetConfigResponse
|
||||||
(*AddProfileResponse)(nil), // 65: daemon.AddProfileResponse
|
(*AddProfileRequest)(nil), // 65: daemon.AddProfileRequest
|
||||||
(*RemoveProfileRequest)(nil), // 66: daemon.RemoveProfileRequest
|
(*AddProfileResponse)(nil), // 66: daemon.AddProfileResponse
|
||||||
(*RemoveProfileResponse)(nil), // 67: daemon.RemoveProfileResponse
|
(*RemoveProfileRequest)(nil), // 67: daemon.RemoveProfileRequest
|
||||||
(*ListProfilesRequest)(nil), // 68: daemon.ListProfilesRequest
|
(*RemoveProfileResponse)(nil), // 68: daemon.RemoveProfileResponse
|
||||||
(*ListProfilesResponse)(nil), // 69: daemon.ListProfilesResponse
|
(*ListProfilesRequest)(nil), // 69: daemon.ListProfilesRequest
|
||||||
(*Profile)(nil), // 70: daemon.Profile
|
(*ListProfilesResponse)(nil), // 70: daemon.ListProfilesResponse
|
||||||
(*GetActiveProfileRequest)(nil), // 71: daemon.GetActiveProfileRequest
|
(*Profile)(nil), // 71: daemon.Profile
|
||||||
(*GetActiveProfileResponse)(nil), // 72: daemon.GetActiveProfileResponse
|
(*GetActiveProfileRequest)(nil), // 72: daemon.GetActiveProfileRequest
|
||||||
(*LogoutRequest)(nil), // 73: daemon.LogoutRequest
|
(*GetActiveProfileResponse)(nil), // 73: daemon.GetActiveProfileResponse
|
||||||
(*LogoutResponse)(nil), // 74: daemon.LogoutResponse
|
(*LogoutRequest)(nil), // 74: daemon.LogoutRequest
|
||||||
(*GetFeaturesRequest)(nil), // 75: daemon.GetFeaturesRequest
|
(*LogoutResponse)(nil), // 75: daemon.LogoutResponse
|
||||||
(*GetFeaturesResponse)(nil), // 76: daemon.GetFeaturesResponse
|
(*GetFeaturesRequest)(nil), // 76: daemon.GetFeaturesRequest
|
||||||
(*GetPeerSSHHostKeyRequest)(nil), // 77: daemon.GetPeerSSHHostKeyRequest
|
(*GetFeaturesResponse)(nil), // 77: daemon.GetFeaturesResponse
|
||||||
(*GetPeerSSHHostKeyResponse)(nil), // 78: daemon.GetPeerSSHHostKeyResponse
|
(*GetPeerSSHHostKeyRequest)(nil), // 78: daemon.GetPeerSSHHostKeyRequest
|
||||||
(*RequestJWTAuthRequest)(nil), // 79: daemon.RequestJWTAuthRequest
|
(*GetPeerSSHHostKeyResponse)(nil), // 79: daemon.GetPeerSSHHostKeyResponse
|
||||||
(*RequestJWTAuthResponse)(nil), // 80: daemon.RequestJWTAuthResponse
|
(*RequestJWTAuthRequest)(nil), // 80: daemon.RequestJWTAuthRequest
|
||||||
(*WaitJWTTokenRequest)(nil), // 81: daemon.WaitJWTTokenRequest
|
(*RequestJWTAuthResponse)(nil), // 81: daemon.RequestJWTAuthResponse
|
||||||
(*WaitJWTTokenResponse)(nil), // 82: daemon.WaitJWTTokenResponse
|
(*WaitJWTTokenRequest)(nil), // 82: daemon.WaitJWTTokenRequest
|
||||||
(*StartCPUProfileRequest)(nil), // 83: daemon.StartCPUProfileRequest
|
(*WaitJWTTokenResponse)(nil), // 83: daemon.WaitJWTTokenResponse
|
||||||
(*StartCPUProfileResponse)(nil), // 84: daemon.StartCPUProfileResponse
|
(*StartCPUProfileRequest)(nil), // 84: daemon.StartCPUProfileRequest
|
||||||
(*StopCPUProfileRequest)(nil), // 85: daemon.StopCPUProfileRequest
|
(*StartCPUProfileResponse)(nil), // 85: daemon.StartCPUProfileResponse
|
||||||
(*StopCPUProfileResponse)(nil), // 86: daemon.StopCPUProfileResponse
|
(*StopCPUProfileRequest)(nil), // 86: daemon.StopCPUProfileRequest
|
||||||
(*InstallerResultRequest)(nil), // 87: daemon.InstallerResultRequest
|
(*StopCPUProfileResponse)(nil), // 87: daemon.StopCPUProfileResponse
|
||||||
(*InstallerResultResponse)(nil), // 88: daemon.InstallerResultResponse
|
(*InstallerResultRequest)(nil), // 88: daemon.InstallerResultRequest
|
||||||
nil, // 89: daemon.Network.ResolvedIPsEntry
|
(*InstallerResultResponse)(nil), // 89: daemon.InstallerResultResponse
|
||||||
(*PortInfo_Range)(nil), // 90: daemon.PortInfo.Range
|
(*ExposeServiceRequest)(nil), // 90: daemon.ExposeServiceRequest
|
||||||
nil, // 91: daemon.SystemEvent.MetadataEntry
|
(*ExposeServiceEvent)(nil), // 91: daemon.ExposeServiceEvent
|
||||||
(*durationpb.Duration)(nil), // 92: google.protobuf.Duration
|
(*ExposeServiceReady)(nil), // 92: daemon.ExposeServiceReady
|
||||||
(*timestamppb.Timestamp)(nil), // 93: google.protobuf.Timestamp
|
nil, // 93: daemon.Network.ResolvedIPsEntry
|
||||||
|
(*PortInfo_Range)(nil), // 94: daemon.PortInfo.Range
|
||||||
|
nil, // 95: daemon.SystemEvent.MetadataEntry
|
||||||
|
(*durationpb.Duration)(nil), // 96: google.protobuf.Duration
|
||||||
|
(*timestamppb.Timestamp)(nil), // 97: google.protobuf.Timestamp
|
||||||
}
|
}
|
||||||
var file_daemon_proto_depIdxs = []int32{
|
var file_daemon_proto_depIdxs = []int32{
|
||||||
1, // 0: daemon.OSLifecycleRequest.type:type_name -> daemon.OSLifecycleRequest.CycleType
|
2, // 0: daemon.OSLifecycleRequest.type:type_name -> daemon.OSLifecycleRequest.CycleType
|
||||||
92, // 1: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
|
96, // 1: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
|
||||||
27, // 2: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
|
28, // 2: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
|
||||||
93, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
|
97, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
|
||||||
93, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
|
97, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
|
||||||
92, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration
|
96, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration
|
||||||
25, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo
|
26, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo
|
||||||
22, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
|
23, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
|
||||||
21, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState
|
22, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState
|
||||||
20, // 9: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState
|
21, // 9: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState
|
||||||
19, // 10: daemon.FullStatus.peers:type_name -> daemon.PeerState
|
20, // 10: daemon.FullStatus.peers:type_name -> daemon.PeerState
|
||||||
23, // 11: daemon.FullStatus.relays:type_name -> daemon.RelayState
|
24, // 11: daemon.FullStatus.relays:type_name -> daemon.RelayState
|
||||||
24, // 12: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState
|
25, // 12: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState
|
||||||
57, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent
|
58, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent
|
||||||
26, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState
|
27, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState
|
||||||
33, // 15: daemon.ListNetworksResponse.routes:type_name -> daemon.Network
|
34, // 15: daemon.ListNetworksResponse.routes:type_name -> daemon.Network
|
||||||
89, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry
|
93, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry
|
||||||
90, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range
|
94, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range
|
||||||
34, // 18: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo
|
35, // 18: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo
|
||||||
34, // 19: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo
|
35, // 19: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo
|
||||||
35, // 20: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule
|
36, // 20: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule
|
||||||
0, // 21: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel
|
0, // 21: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel
|
||||||
0, // 22: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel
|
0, // 22: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel
|
||||||
43, // 23: daemon.ListStatesResponse.states:type_name -> daemon.State
|
44, // 23: daemon.ListStatesResponse.states:type_name -> daemon.State
|
||||||
52, // 24: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags
|
53, // 24: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags
|
||||||
54, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage
|
55, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage
|
||||||
2, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity
|
3, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity
|
||||||
3, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category
|
4, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category
|
||||||
93, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp
|
97, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||||
91, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry
|
95, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry
|
||||||
57, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent
|
58, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent
|
||||||
92, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
|
96, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
|
||||||
70, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile
|
71, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile
|
||||||
32, // 33: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList
|
1, // 33: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol
|
||||||
7, // 34: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
|
92, // 34: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady
|
||||||
9, // 35: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
|
33, // 35: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList
|
||||||
11, // 36: daemon.DaemonService.Up:input_type -> daemon.UpRequest
|
8, // 36: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
|
||||||
13, // 37: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
|
10, // 37: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
|
||||||
15, // 38: daemon.DaemonService.Down:input_type -> daemon.DownRequest
|
12, // 38: daemon.DaemonService.Up:input_type -> daemon.UpRequest
|
||||||
17, // 39: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
|
14, // 39: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
|
||||||
28, // 40: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest
|
16, // 40: daemon.DaemonService.Down:input_type -> daemon.DownRequest
|
||||||
30, // 41: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest
|
18, // 41: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
|
||||||
30, // 42: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest
|
29, // 42: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest
|
||||||
4, // 43: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest
|
31, // 43: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest
|
||||||
37, // 44: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest
|
31, // 44: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest
|
||||||
39, // 45: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest
|
5, // 45: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest
|
||||||
41, // 46: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest
|
38, // 46: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest
|
||||||
44, // 47: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest
|
40, // 47: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest
|
||||||
46, // 48: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest
|
42, // 48: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest
|
||||||
48, // 49: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest
|
45, // 49: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest
|
||||||
50, // 50: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest
|
47, // 50: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest
|
||||||
53, // 51: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest
|
49, // 51: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest
|
||||||
56, // 52: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest
|
51, // 52: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest
|
||||||
58, // 53: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest
|
54, // 53: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest
|
||||||
60, // 54: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest
|
57, // 54: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest
|
||||||
62, // 55: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest
|
59, // 55: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest
|
||||||
64, // 56: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest
|
61, // 56: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest
|
||||||
66, // 57: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest
|
63, // 57: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest
|
||||||
68, // 58: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest
|
65, // 58: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest
|
||||||
71, // 59: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest
|
67, // 59: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest
|
||||||
73, // 60: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest
|
69, // 60: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest
|
||||||
75, // 61: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest
|
72, // 61: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest
|
||||||
77, // 62: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest
|
74, // 62: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest
|
||||||
79, // 63: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest
|
76, // 63: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest
|
||||||
81, // 64: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest
|
78, // 64: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest
|
||||||
83, // 65: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest
|
80, // 65: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest
|
||||||
85, // 66: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest
|
82, // 66: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest
|
||||||
5, // 67: daemon.DaemonService.NotifyOSLifecycle:input_type -> daemon.OSLifecycleRequest
|
84, // 67: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest
|
||||||
87, // 68: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest
|
86, // 68: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest
|
||||||
8, // 69: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
|
6, // 69: daemon.DaemonService.NotifyOSLifecycle:input_type -> daemon.OSLifecycleRequest
|
||||||
10, // 70: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
|
88, // 70: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest
|
||||||
12, // 71: daemon.DaemonService.Up:output_type -> daemon.UpResponse
|
90, // 71: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest
|
||||||
14, // 72: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
|
9, // 72: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
|
||||||
16, // 73: daemon.DaemonService.Down:output_type -> daemon.DownResponse
|
11, // 73: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
|
||||||
18, // 74: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
|
13, // 74: daemon.DaemonService.Up:output_type -> daemon.UpResponse
|
||||||
29, // 75: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse
|
15, // 75: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
|
||||||
31, // 76: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse
|
17, // 76: daemon.DaemonService.Down:output_type -> daemon.DownResponse
|
||||||
31, // 77: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse
|
19, // 77: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
|
||||||
36, // 78: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse
|
30, // 78: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse
|
||||||
38, // 79: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse
|
32, // 79: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse
|
||||||
40, // 80: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse
|
32, // 80: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse
|
||||||
42, // 81: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse
|
37, // 81: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse
|
||||||
45, // 82: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse
|
39, // 82: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse
|
||||||
47, // 83: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse
|
41, // 83: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse
|
||||||
49, // 84: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse
|
43, // 84: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse
|
||||||
51, // 85: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse
|
46, // 85: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse
|
||||||
55, // 86: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse
|
48, // 86: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse
|
||||||
57, // 87: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent
|
50, // 87: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse
|
||||||
59, // 88: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse
|
52, // 88: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse
|
||||||
61, // 89: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse
|
56, // 89: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse
|
||||||
63, // 90: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse
|
58, // 90: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent
|
||||||
65, // 91: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse
|
60, // 91: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse
|
||||||
67, // 92: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse
|
62, // 92: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse
|
||||||
69, // 93: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse
|
64, // 93: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse
|
||||||
72, // 94: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse
|
66, // 94: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse
|
||||||
74, // 95: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse
|
68, // 95: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse
|
||||||
76, // 96: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse
|
70, // 96: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse
|
||||||
78, // 97: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse
|
73, // 97: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse
|
||||||
80, // 98: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse
|
75, // 98: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse
|
||||||
82, // 99: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse
|
77, // 99: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse
|
||||||
84, // 100: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse
|
79, // 100: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse
|
||||||
86, // 101: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse
|
81, // 101: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse
|
||||||
6, // 102: daemon.DaemonService.NotifyOSLifecycle:output_type -> daemon.OSLifecycleResponse
|
83, // 102: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse
|
||||||
88, // 103: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse
|
85, // 103: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse
|
||||||
69, // [69:104] is the sub-list for method output_type
|
87, // 104: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse
|
||||||
34, // [34:69] is the sub-list for method input_type
|
7, // 105: daemon.DaemonService.NotifyOSLifecycle:output_type -> daemon.OSLifecycleResponse
|
||||||
34, // [34:34] is the sub-list for extension type_name
|
89, // 106: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse
|
||||||
34, // [34:34] is the sub-list for extension extendee
|
91, // 107: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent
|
||||||
0, // [0:34] is the sub-list for field type_name
|
72, // [72:108] is the sub-list for method output_type
|
||||||
|
36, // [36:72] is the sub-list for method input_type
|
||||||
|
36, // [36:36] is the sub-list for extension type_name
|
||||||
|
36, // [36:36] is the sub-list for extension extendee
|
||||||
|
0, // [0:36] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_daemon_proto_init() }
|
func init() { file_daemon_proto_init() }
|
||||||
@@ -6439,13 +6743,16 @@ func file_daemon_proto_init() {
|
|||||||
file_daemon_proto_msgTypes[58].OneofWrappers = []any{}
|
file_daemon_proto_msgTypes[58].OneofWrappers = []any{}
|
||||||
file_daemon_proto_msgTypes[69].OneofWrappers = []any{}
|
file_daemon_proto_msgTypes[69].OneofWrappers = []any{}
|
||||||
file_daemon_proto_msgTypes[75].OneofWrappers = []any{}
|
file_daemon_proto_msgTypes[75].OneofWrappers = []any{}
|
||||||
|
file_daemon_proto_msgTypes[86].OneofWrappers = []any{
|
||||||
|
(*ExposeServiceEvent_Ready)(nil),
|
||||||
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)),
|
||||||
NumEnums: 4,
|
NumEnums: 5,
|
||||||
NumMessages: 88,
|
NumMessages: 91,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ service DaemonService {
|
|||||||
rpc NotifyOSLifecycle(OSLifecycleRequest) returns(OSLifecycleResponse) {}
|
rpc NotifyOSLifecycle(OSLifecycleRequest) returns(OSLifecycleResponse) {}
|
||||||
|
|
||||||
rpc GetInstallerResult(InstallerResultRequest) returns (InstallerResultResponse) {}
|
rpc GetInstallerResult(InstallerResultRequest) returns (InstallerResultResponse) {}
|
||||||
|
|
||||||
|
// ExposeService exposes a local port via the NetBird reverse proxy
|
||||||
|
rpc ExposeService(ExposeServiceRequest) returns (stream ExposeServiceEvent) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -801,3 +804,32 @@ message InstallerResultResponse {
|
|||||||
bool success = 1;
|
bool success = 1;
|
||||||
string errorMsg = 2;
|
string errorMsg = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ExposeProtocol {
|
||||||
|
EXPOSE_HTTP = 0;
|
||||||
|
EXPOSE_HTTPS = 1;
|
||||||
|
EXPOSE_TCP = 2;
|
||||||
|
EXPOSE_UDP = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ExposeServiceRequest {
|
||||||
|
uint32 port = 1;
|
||||||
|
ExposeProtocol protocol = 2;
|
||||||
|
string pin = 3;
|
||||||
|
string password = 4;
|
||||||
|
repeated string user_groups = 5;
|
||||||
|
string domain = 6;
|
||||||
|
string name_prefix = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ExposeServiceEvent {
|
||||||
|
oneof event {
|
||||||
|
ExposeServiceReady ready = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message ExposeServiceReady {
|
||||||
|
string service_name = 1;
|
||||||
|
string service_url = 2;
|
||||||
|
string domain = 3;
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ type DaemonServiceClient interface {
|
|||||||
StopCPUProfile(ctx context.Context, in *StopCPUProfileRequest, opts ...grpc.CallOption) (*StopCPUProfileResponse, error)
|
StopCPUProfile(ctx context.Context, in *StopCPUProfileRequest, opts ...grpc.CallOption) (*StopCPUProfileResponse, error)
|
||||||
NotifyOSLifecycle(ctx context.Context, in *OSLifecycleRequest, opts ...grpc.CallOption) (*OSLifecycleResponse, error)
|
NotifyOSLifecycle(ctx context.Context, in *OSLifecycleRequest, opts ...grpc.CallOption) (*OSLifecycleResponse, error)
|
||||||
GetInstallerResult(ctx context.Context, in *InstallerResultRequest, opts ...grpc.CallOption) (*InstallerResultResponse, error)
|
GetInstallerResult(ctx context.Context, in *InstallerResultRequest, opts ...grpc.CallOption) (*InstallerResultResponse, error)
|
||||||
|
// ExposeService exposes a local port via the NetBird reverse proxy
|
||||||
|
ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (DaemonService_ExposeServiceClient, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type daemonServiceClient struct {
|
type daemonServiceClient struct {
|
||||||
@@ -424,6 +426,38 @@ func (c *daemonServiceClient) GetInstallerResult(ctx context.Context, in *Instal
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (DaemonService_ExposeServiceClient, error) {
|
||||||
|
stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[1], "/daemon.DaemonService/ExposeService", opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &daemonServiceExposeServiceClient{stream}
|
||||||
|
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := x.ClientStream.CloseSend(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DaemonService_ExposeServiceClient interface {
|
||||||
|
Recv() (*ExposeServiceEvent, error)
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type daemonServiceExposeServiceClient struct {
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *daemonServiceExposeServiceClient) Recv() (*ExposeServiceEvent, error) {
|
||||||
|
m := new(ExposeServiceEvent)
|
||||||
|
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DaemonServiceServer is the server API for DaemonService service.
|
// DaemonServiceServer is the server API for DaemonService service.
|
||||||
// All implementations must embed UnimplementedDaemonServiceServer
|
// All implementations must embed UnimplementedDaemonServiceServer
|
||||||
// for forward compatibility
|
// for forward compatibility
|
||||||
@@ -486,6 +520,8 @@ type DaemonServiceServer interface {
|
|||||||
StopCPUProfile(context.Context, *StopCPUProfileRequest) (*StopCPUProfileResponse, error)
|
StopCPUProfile(context.Context, *StopCPUProfileRequest) (*StopCPUProfileResponse, error)
|
||||||
NotifyOSLifecycle(context.Context, *OSLifecycleRequest) (*OSLifecycleResponse, error)
|
NotifyOSLifecycle(context.Context, *OSLifecycleRequest) (*OSLifecycleResponse, error)
|
||||||
GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error)
|
GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error)
|
||||||
|
// ExposeService exposes a local port via the NetBird reverse proxy
|
||||||
|
ExposeService(*ExposeServiceRequest, DaemonService_ExposeServiceServer) error
|
||||||
mustEmbedUnimplementedDaemonServiceServer()
|
mustEmbedUnimplementedDaemonServiceServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,6 +634,9 @@ func (UnimplementedDaemonServiceServer) NotifyOSLifecycle(context.Context, *OSLi
|
|||||||
func (UnimplementedDaemonServiceServer) GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error) {
|
func (UnimplementedDaemonServiceServer) GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetInstallerResult not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method GetInstallerResult not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) ExposeService(*ExposeServiceRequest, DaemonService_ExposeServiceServer) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method ExposeService not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {}
|
func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {}
|
||||||
|
|
||||||
// UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service.
|
// UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
@@ -1244,6 +1283,27 @@ func _DaemonService_GetInstallerResult_Handler(srv interface{}, ctx context.Cont
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _DaemonService_ExposeService_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
m := new(ExposeServiceRequest)
|
||||||
|
if err := stream.RecvMsg(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return srv.(DaemonServiceServer).ExposeService(m, &daemonServiceExposeServiceServer{stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
type DaemonService_ExposeServiceServer interface {
|
||||||
|
Send(*ExposeServiceEvent) error
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type daemonServiceExposeServiceServer struct {
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *daemonServiceExposeServiceServer) Send(m *ExposeServiceEvent) error {
|
||||||
|
return x.ServerStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
// DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service.
|
// DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -1394,6 +1454,11 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
Handler: _DaemonService_SubscribeEvents_Handler,
|
Handler: _DaemonService_SubscribeEvents_Handler,
|
||||||
ServerStreams: true,
|
ServerStreams: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
StreamName: "ExposeService",
|
||||||
|
Handler: _DaemonService_ExposeService_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Metadata: "daemon.proto",
|
Metadata: "daemon.proto",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NotifyOSLifecycle handles operating system lifecycle events by executing appropriate logic based on the request type.
|
|
||||||
func (s *Server) NotifyOSLifecycle(callerCtx context.Context, req *proto.OSLifecycleRequest) (*proto.OSLifecycleResponse, error) {
|
|
||||||
switch req.GetType() {
|
|
||||||
case proto.OSLifecycleRequest_WAKEUP:
|
|
||||||
return s.handleWakeUp(callerCtx)
|
|
||||||
case proto.OSLifecycleRequest_SLEEP:
|
|
||||||
return s.handleSleep(callerCtx)
|
|
||||||
default:
|
|
||||||
log.Errorf("unknown OSLifecycleRequest type: %v", req.GetType())
|
|
||||||
}
|
|
||||||
return &proto.OSLifecycleResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleWakeUp processes a wake-up event by triggering the Up command if the system was previously put to sleep.
|
|
||||||
// It resets the sleep state and logs the process. Returns a response or an error if the Up command fails.
|
|
||||||
func (s *Server) handleWakeUp(callerCtx context.Context) (*proto.OSLifecycleResponse, error) {
|
|
||||||
if !s.sleepTriggeredDown.Load() {
|
|
||||||
log.Info("skipping up because wasn't sleep down")
|
|
||||||
return &proto.OSLifecycleResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// avoid other wakeup runs if sleep didn't make the computer sleep
|
|
||||||
s.sleepTriggeredDown.Store(false)
|
|
||||||
|
|
||||||
log.Info("running up after wake up")
|
|
||||||
_, err := s.Up(callerCtx, &proto.UpRequest{})
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("running up failed: %v", err)
|
|
||||||
return &proto.OSLifecycleResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("running up command executed successfully")
|
|
||||||
return &proto.OSLifecycleResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleSleep handles the sleep event by initiating a "down" sequence if the system is in a connected or connecting state.
|
|
||||||
func (s *Server) handleSleep(callerCtx context.Context) (*proto.OSLifecycleResponse, error) {
|
|
||||||
s.mutex.Lock()
|
|
||||||
|
|
||||||
state := internal.CtxGetState(s.rootCtx)
|
|
||||||
status, err := state.Status()
|
|
||||||
if err != nil {
|
|
||||||
s.mutex.Unlock()
|
|
||||||
return &proto.OSLifecycleResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if status != internal.StatusConnecting && status != internal.StatusConnected {
|
|
||||||
log.Infof("skipping setting the agent down because status is %s", status)
|
|
||||||
s.mutex.Unlock()
|
|
||||||
return &proto.OSLifecycleResponse{}, nil
|
|
||||||
}
|
|
||||||
s.mutex.Unlock()
|
|
||||||
|
|
||||||
log.Info("running down after system started sleeping")
|
|
||||||
|
|
||||||
_, err = s.Down(callerCtx, &proto.DownRequest{})
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("running down failed: %v", err)
|
|
||||||
return &proto.OSLifecycleResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.sleepTriggeredDown.Store(true)
|
|
||||||
|
|
||||||
log.Info("running down executed successfully")
|
|
||||||
return &proto.OSLifecycleResponse{}, nil
|
|
||||||
}
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newTestServer() *Server {
|
|
||||||
ctx := internal.CtxInitState(context.Background())
|
|
||||||
return &Server{
|
|
||||||
rootCtx: ctx,
|
|
||||||
statusRecorder: peer.NewRecorder(""),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotifyOSLifecycle_WakeUp_SkipsWhenNotSleepTriggered(t *testing.T) {
|
|
||||||
s := newTestServer()
|
|
||||||
|
|
||||||
// sleepTriggeredDown is false by default
|
|
||||||
assert.False(t, s.sleepTriggeredDown.Load())
|
|
||||||
|
|
||||||
resp, err := s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{
|
|
||||||
Type: proto.OSLifecycleRequest_WAKEUP,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, resp)
|
|
||||||
assert.False(t, s.sleepTriggeredDown.Load(), "flag should remain false")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotifyOSLifecycle_Sleep_SkipsWhenStatusIdle(t *testing.T) {
|
|
||||||
s := newTestServer()
|
|
||||||
|
|
||||||
state := internal.CtxGetState(s.rootCtx)
|
|
||||||
state.Set(internal.StatusIdle)
|
|
||||||
|
|
||||||
resp, err := s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{
|
|
||||||
Type: proto.OSLifecycleRequest_SLEEP,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, resp)
|
|
||||||
assert.False(t, s.sleepTriggeredDown.Load(), "flag should remain false when status is Idle")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotifyOSLifecycle_Sleep_SkipsWhenStatusNeedsLogin(t *testing.T) {
|
|
||||||
s := newTestServer()
|
|
||||||
|
|
||||||
state := internal.CtxGetState(s.rootCtx)
|
|
||||||
state.Set(internal.StatusNeedsLogin)
|
|
||||||
|
|
||||||
resp, err := s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{
|
|
||||||
Type: proto.OSLifecycleRequest_SLEEP,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, resp)
|
|
||||||
assert.False(t, s.sleepTriggeredDown.Load(), "flag should remain false when status is NeedsLogin")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotifyOSLifecycle_Sleep_SetsFlag_WhenConnecting(t *testing.T) {
|
|
||||||
s := newTestServer()
|
|
||||||
|
|
||||||
state := internal.CtxGetState(s.rootCtx)
|
|
||||||
state.Set(internal.StatusConnecting)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
s.actCancel = cancel
|
|
||||||
|
|
||||||
resp, err := s.NotifyOSLifecycle(ctx, &proto.OSLifecycleRequest{
|
|
||||||
Type: proto.OSLifecycleRequest_SLEEP,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, resp, "handleSleep returns not nil response on success")
|
|
||||||
assert.True(t, s.sleepTriggeredDown.Load(), "flag should be set after sleep when connecting")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotifyOSLifecycle_Sleep_SetsFlag_WhenConnected(t *testing.T) {
|
|
||||||
s := newTestServer()
|
|
||||||
|
|
||||||
state := internal.CtxGetState(s.rootCtx)
|
|
||||||
state.Set(internal.StatusConnected)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
s.actCancel = cancel
|
|
||||||
|
|
||||||
resp, err := s.NotifyOSLifecycle(ctx, &proto.OSLifecycleRequest{
|
|
||||||
Type: proto.OSLifecycleRequest_SLEEP,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, resp, "handleSleep returns not nil response on success")
|
|
||||||
assert.True(t, s.sleepTriggeredDown.Load(), "flag should be set after sleep when connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotifyOSLifecycle_WakeUp_ResetsFlag(t *testing.T) {
|
|
||||||
s := newTestServer()
|
|
||||||
|
|
||||||
// Manually set the flag to simulate prior sleep down
|
|
||||||
s.sleepTriggeredDown.Store(true)
|
|
||||||
|
|
||||||
// WakeUp will try to call Up which fails without proper setup, but flag should reset first
|
|
||||||
_, _ = s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{
|
|
||||||
Type: proto.OSLifecycleRequest_WAKEUP,
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.False(t, s.sleepTriggeredDown.Load(), "flag should be reset after WakeUp attempt")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotifyOSLifecycle_MultipleWakeUpCalls(t *testing.T) {
|
|
||||||
s := newTestServer()
|
|
||||||
|
|
||||||
// First wakeup without prior sleep - should be no-op
|
|
||||||
resp, err := s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{
|
|
||||||
Type: proto.OSLifecycleRequest_WAKEUP,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, resp)
|
|
||||||
assert.False(t, s.sleepTriggeredDown.Load())
|
|
||||||
|
|
||||||
// Simulate prior sleep
|
|
||||||
s.sleepTriggeredDown.Store(true)
|
|
||||||
|
|
||||||
// First wakeup after sleep - should reset flag
|
|
||||||
_, _ = s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{
|
|
||||||
Type: proto.OSLifecycleRequest_WAKEUP,
|
|
||||||
})
|
|
||||||
assert.False(t, s.sleepTriggeredDown.Load())
|
|
||||||
|
|
||||||
// Second wakeup - should be no-op
|
|
||||||
resp, err = s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{
|
|
||||||
Type: proto.OSLifecycleRequest_WAKEUP,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, resp)
|
|
||||||
assert.False(t, s.sleepTriggeredDown.Load())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleWakeUp_SkipsWhenFlagFalse(t *testing.T) {
|
|
||||||
s := newTestServer()
|
|
||||||
|
|
||||||
resp, err := s.handleWakeUp(context.Background())
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleWakeUp_ResetsFlagBeforeUp(t *testing.T) {
|
|
||||||
s := newTestServer()
|
|
||||||
s.sleepTriggeredDown.Store(true)
|
|
||||||
|
|
||||||
// Even if Up fails, flag should be reset
|
|
||||||
_, _ = s.handleWakeUp(context.Background())
|
|
||||||
|
|
||||||
assert.False(t, s.sleepTriggeredDown.Load(), "flag must be reset before calling Up")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleSleep_SkipsForNonActiveStates(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
status internal.StatusType
|
|
||||||
}{
|
|
||||||
{"Idle", internal.StatusIdle},
|
|
||||||
{"NeedsLogin", internal.StatusNeedsLogin},
|
|
||||||
{"LoginFailed", internal.StatusLoginFailed},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
s := newTestServer()
|
|
||||||
state := internal.CtxGetState(s.rootCtx)
|
|
||||||
state.Set(tt.status)
|
|
||||||
|
|
||||||
resp, err := s.handleSleep(context.Background())
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, resp)
|
|
||||||
assert.False(t, s.sleepTriggeredDown.Load())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleSleep_ProceedsForActiveStates(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
status internal.StatusType
|
|
||||||
}{
|
|
||||||
{"Connecting", internal.StatusConnecting},
|
|
||||||
{"Connected", internal.StatusConnected},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
s := newTestServer()
|
|
||||||
state := internal.CtxGetState(s.rootCtx)
|
|
||||||
state.Set(tt.status)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
s.actCancel = cancel
|
|
||||||
|
|
||||||
resp, err := s.handleSleep(ctx)
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, resp)
|
|
||||||
assert.True(t, s.sleepTriggeredDown.Load())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,9 @@ import (
|
|||||||
gstatus "google.golang.org/grpc/status"
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/auth"
|
"github.com/netbirdio/netbird/client/internal/auth"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/expose"
|
||||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
|
sleephandler "github.com/netbirdio/netbird/client/internal/sleep/handler"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
mgm "github.com/netbirdio/netbird/shared/management/client"
|
mgm "github.com/netbirdio/netbird/shared/management/client"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
@@ -85,8 +87,7 @@ type Server struct {
|
|||||||
profilesDisabled bool
|
profilesDisabled bool
|
||||||
updateSettingsDisabled bool
|
updateSettingsDisabled bool
|
||||||
|
|
||||||
// sleepTriggeredDown holds a state indicated if the sleep handler triggered the last client down
|
sleepHandler *sleephandler.SleepHandler
|
||||||
sleepTriggeredDown atomic.Bool
|
|
||||||
|
|
||||||
jwtCache *jwtCache
|
jwtCache *jwtCache
|
||||||
}
|
}
|
||||||
@@ -100,7 +101,7 @@ type oauthAuthFlow struct {
|
|||||||
|
|
||||||
// New server instance constructor.
|
// New server instance constructor.
|
||||||
func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool, updateSettingsDisabled bool) *Server {
|
func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool, updateSettingsDisabled bool) *Server {
|
||||||
return &Server{
|
s := &Server{
|
||||||
rootCtx: ctx,
|
rootCtx: ctx,
|
||||||
logFile: logFile,
|
logFile: logFile,
|
||||||
persistSyncResponse: true,
|
persistSyncResponse: true,
|
||||||
@@ -110,6 +111,10 @@ func New(ctx context.Context, logFile string, configFile string, profilesDisable
|
|||||||
updateSettingsDisabled: updateSettingsDisabled,
|
updateSettingsDisabled: updateSettingsDisabled,
|
||||||
jwtCache: newJWTCache(),
|
jwtCache: newJWTCache(),
|
||||||
}
|
}
|
||||||
|
agent := &serverAgent{s}
|
||||||
|
s.sleepHandler = sleephandler.New(agent)
|
||||||
|
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
@@ -636,8 +641,6 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
|
|||||||
|
|
||||||
return s.waitForUp(callerCtx)
|
return s.waitForUp(callerCtx)
|
||||||
}
|
}
|
||||||
defer s.mutex.Unlock()
|
|
||||||
|
|
||||||
if err := restoreResidualState(callerCtx, s.profileManager.GetStatePath()); err != nil {
|
if err := restoreResidualState(callerCtx, s.profileManager.GetStatePath()); err != nil {
|
||||||
log.Warnf(errRestoreResidualState, err)
|
log.Warnf(errRestoreResidualState, err)
|
||||||
}
|
}
|
||||||
@@ -649,10 +652,12 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
|
|||||||
// not in the progress or already successfully established connection.
|
// not in the progress or already successfully established connection.
|
||||||
status, err := state.Status()
|
status, err := state.Status()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mutex.Unlock()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if status != internal.StatusIdle {
|
if status != internal.StatusIdle {
|
||||||
|
s.mutex.Unlock()
|
||||||
return nil, fmt.Errorf("up already in progress: current status %s", status)
|
return nil, fmt.Errorf("up already in progress: current status %s", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,17 +674,20 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
|
|||||||
s.actCancel = cancel
|
s.actCancel = cancel
|
||||||
|
|
||||||
if s.config == nil {
|
if s.config == nil {
|
||||||
|
s.mutex.Unlock()
|
||||||
return nil, fmt.Errorf("config is not defined, please call login command first")
|
return nil, fmt.Errorf("config is not defined, please call login command first")
|
||||||
}
|
}
|
||||||
|
|
||||||
activeProf, err := s.profileManager.GetActiveProfileState()
|
activeProf, err := s.profileManager.GetActiveProfileState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mutex.Unlock()
|
||||||
log.Errorf("failed to get active profile state: %v", err)
|
log.Errorf("failed to get active profile state: %v", err)
|
||||||
return nil, fmt.Errorf("failed to get active profile state: %w", err)
|
return nil, fmt.Errorf("failed to get active profile state: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg != nil && msg.ProfileName != nil {
|
if msg != nil && msg.ProfileName != nil {
|
||||||
if err := s.switchProfileIfNeeded(*msg.ProfileName, msg.Username, activeProf); err != nil {
|
if err := s.switchProfileIfNeeded(*msg.ProfileName, msg.Username, activeProf); err != nil {
|
||||||
|
s.mutex.Unlock()
|
||||||
log.Errorf("failed to switch profile: %v", err)
|
log.Errorf("failed to switch profile: %v", err)
|
||||||
return nil, fmt.Errorf("failed to switch profile: %w", err)
|
return nil, fmt.Errorf("failed to switch profile: %w", err)
|
||||||
}
|
}
|
||||||
@@ -687,6 +695,7 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
|
|||||||
|
|
||||||
activeProf, err = s.profileManager.GetActiveProfileState()
|
activeProf, err = s.profileManager.GetActiveProfileState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mutex.Unlock()
|
||||||
log.Errorf("failed to get active profile state: %v", err)
|
log.Errorf("failed to get active profile state: %v", err)
|
||||||
return nil, fmt.Errorf("failed to get active profile state: %w", err)
|
return nil, fmt.Errorf("failed to get active profile state: %w", err)
|
||||||
}
|
}
|
||||||
@@ -695,6 +704,7 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
|
|||||||
|
|
||||||
config, _, err := s.getConfig(activeProf)
|
config, _, err := s.getConfig(activeProf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mutex.Unlock()
|
||||||
log.Errorf("failed to get active profile config: %v", err)
|
log.Errorf("failed to get active profile config: %v", err)
|
||||||
return nil, fmt.Errorf("failed to get active profile config: %w", err)
|
return nil, fmt.Errorf("failed to get active profile config: %w", err)
|
||||||
}
|
}
|
||||||
@@ -713,6 +723,7 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
|
|||||||
}
|
}
|
||||||
go s.connectWithRetryRuns(ctx, s.config, s.statusRecorder, doAutoUpdate, s.clientRunningChan, s.clientGiveUpChan)
|
go s.connectWithRetryRuns(ctx, s.config, s.statusRecorder, doAutoUpdate, s.clientRunningChan, s.clientGiveUpChan)
|
||||||
|
|
||||||
|
s.mutex.Unlock()
|
||||||
return s.waitForUp(callerCtx)
|
return s.waitForUp(callerCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1312,6 +1323,60 @@ func (s *Server) WaitJWTToken(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExposeService exposes a local port via the NetBird reverse proxy.
|
||||||
|
func (s *Server) ExposeService(req *proto.ExposeServiceRequest, srv proto.DaemonService_ExposeServiceServer) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
if !s.clientRunning {
|
||||||
|
s.mutex.Unlock()
|
||||||
|
return gstatus.Errorf(codes.FailedPrecondition, "client is not running, run 'netbird up' first")
|
||||||
|
}
|
||||||
|
connectClient := s.connectClient
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
if connectClient == nil {
|
||||||
|
return gstatus.Errorf(codes.FailedPrecondition, "client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
engine := connectClient.Engine()
|
||||||
|
if engine == nil {
|
||||||
|
return gstatus.Errorf(codes.FailedPrecondition, "engine not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr := engine.GetExposeManager()
|
||||||
|
if mgr == nil {
|
||||||
|
return gstatus.Errorf(codes.Internal, "expose manager not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := srv.Context()
|
||||||
|
|
||||||
|
exposeCtx, exposeCancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
defer exposeCancel()
|
||||||
|
|
||||||
|
mgmReq := expose.NewRequest(req)
|
||||||
|
result, err := mgr.Expose(exposeCtx, *mgmReq)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := srv.Send(&proto.ExposeServiceEvent{
|
||||||
|
Event: &proto.ExposeServiceEvent_Ready{
|
||||||
|
Ready: &proto.ExposeServiceReady{
|
||||||
|
ServiceName: result.ServiceName,
|
||||||
|
ServiceUrl: result.ServiceURL,
|
||||||
|
Domain: result.Domain,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mgr.KeepAlive(ctx, result.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func isUnixRunningDesktop() bool {
|
func isUnixRunningDesktop() bool {
|
||||||
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
||||||
return false
|
return false
|
||||||
|
|||||||
46
client/server/sleep.go
Normal file
46
client/server/sleep.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// serverAgent adapts Server to the handler.Agent and handler.StatusChecker interfaces
|
||||||
|
type serverAgent struct {
|
||||||
|
s *Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *serverAgent) Up(ctx context.Context) error {
|
||||||
|
_, err := a.s.Up(ctx, &proto.UpRequest{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *serverAgent) Down(ctx context.Context) error {
|
||||||
|
_, err := a.s.Down(ctx, &proto.DownRequest{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *serverAgent) Status() (internal.StatusType, error) {
|
||||||
|
return internal.CtxGetState(a.s.rootCtx).Status()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyOSLifecycle handles operating system lifecycle events by executing appropriate logic based on the request type.
|
||||||
|
func (s *Server) NotifyOSLifecycle(callerCtx context.Context, req *proto.OSLifecycleRequest) (*proto.OSLifecycleResponse, error) {
|
||||||
|
switch req.GetType() {
|
||||||
|
case proto.OSLifecycleRequest_WAKEUP:
|
||||||
|
if err := s.sleepHandler.HandleWakeUp(callerCtx); err != nil {
|
||||||
|
return &proto.OSLifecycleResponse{}, err
|
||||||
|
}
|
||||||
|
case proto.OSLifecycleRequest_SLEEP:
|
||||||
|
if err := s.sleepHandler.HandleSleep(callerCtx); err != nil {
|
||||||
|
return &proto.OSLifecycleResponse{}, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Errorf("unknown OSLifecycleRequest type: %v", req.GetType())
|
||||||
|
}
|
||||||
|
return &proto.OSLifecycleResponse{}, nil
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/daemonaddr"
|
||||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
nbssh "github.com/netbirdio/netbird/client/ssh"
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
||||||
@@ -268,7 +269,7 @@ func getDefaultDaemonAddr() string {
|
|||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return DefaultDaemonAddrWindows
|
return DefaultDaemonAddrWindows
|
||||||
}
|
}
|
||||||
return DefaultDaemonAddr
|
return daemonaddr.ResolveUnixDaemonAddr(DefaultDaemonAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialOptions contains options for SSH connections
|
// DialOptions contains options for SSH connections
|
||||||
|
|||||||
@@ -46,8 +46,10 @@ const (
|
|||||||
cmdSFTP = "<sftp>"
|
cmdSFTP = "<sftp>"
|
||||||
cmdNonInteractive = "<idle>"
|
cmdNonInteractive = "<idle>"
|
||||||
|
|
||||||
// DefaultJWTMaxTokenAge is the default maximum age for JWT tokens accepted by the SSH server
|
// DefaultJWTMaxTokenAge is the default maximum age for JWT tokens accepted by the SSH server.
|
||||||
DefaultJWTMaxTokenAge = 5 * 60
|
// Set to 10 minutes to accommodate identity providers like Azure Entra ID
|
||||||
|
// that backdate the iat claim by up to 5 minutes.
|
||||||
|
DefaultJWTMaxTokenAge = 10 * 60
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
25
combined/Dockerfile.multistage
Normal file
25
combined/Dockerfile.multistage
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM golang:1.25-bookworm AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apt-get update && apt-get install -y gcc libc6-dev git && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build with version info from git (matching goreleaser ldflags)
|
||||||
|
RUN CGO_ENABLED=1 GOOS=linux go build \
|
||||||
|
-ldflags="-s -w \
|
||||||
|
-X github.com/netbirdio/netbird/version.version=$(git describe --tags --always --dirty 2>/dev/null || echo 'dev') \
|
||||||
|
-X main.commit=$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown') \
|
||||||
|
-X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
|
||||||
|
-X main.builtBy=docker" \
|
||||||
|
-o netbird-server ./combined
|
||||||
|
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
RUN apt update && apt install -y ca-certificates && rm -fr /var/cache/apt
|
||||||
|
ENTRYPOINT [ "/go/bin/netbird-server" ]
|
||||||
|
CMD ["--config", "/etc/netbird/config.yaml"]
|
||||||
|
COPY --from=builder /app/netbird-server /go/bin/netbird-server
|
||||||
661
combined/LICENSE
Normal file
661
combined/LICENSE
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -70,6 +71,8 @@ type ServerConfig struct {
|
|||||||
DisableGeoliteUpdate bool `yaml:"disableGeoliteUpdate"`
|
DisableGeoliteUpdate bool `yaml:"disableGeoliteUpdate"`
|
||||||
Auth AuthConfig `yaml:"auth"`
|
Auth AuthConfig `yaml:"auth"`
|
||||||
Store StoreConfig `yaml:"store"`
|
Store StoreConfig `yaml:"store"`
|
||||||
|
ActivityStore StoreConfig `yaml:"activityStore"`
|
||||||
|
AuthStore StoreConfig `yaml:"authStore"`
|
||||||
ReverseProxy ReverseProxyConfig `yaml:"reverseProxy"`
|
ReverseProxy ReverseProxyConfig `yaml:"reverseProxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +173,8 @@ type RelaysConfig struct {
|
|||||||
type StoreConfig struct {
|
type StoreConfig struct {
|
||||||
Engine string `yaml:"engine"`
|
Engine string `yaml:"engine"`
|
||||||
EncryptionKey string `yaml:"encryptionKey"`
|
EncryptionKey string `yaml:"encryptionKey"`
|
||||||
DSN string `yaml:"dsn"` // Connection string for postgres or mysql engines
|
DSN string `yaml:"dsn"` // Connection string for postgres or mysql engines
|
||||||
|
File string `yaml:"file"` // SQLite database file path (optional, defaults to dataDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReverseProxyConfig contains reverse proxy settings
|
// ReverseProxyConfig contains reverse proxy settings
|
||||||
@@ -532,6 +536,74 @@ func stripSignalProtocol(uri string) string {
|
|||||||
return uri
|
return uri
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {
|
||||||
|
var ttl time.Duration
|
||||||
|
if relays.CredentialsTTL != "" {
|
||||||
|
var err error
|
||||||
|
ttl, err = time.ParseDuration(relays.CredentialsTTL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid relay credentials TTL %q: %w", relays.CredentialsTTL, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &nbconfig.Relay{
|
||||||
|
Addresses: relays.Addresses,
|
||||||
|
CredentialsTTL: util.Duration{Duration: ttl},
|
||||||
|
Secret: relays.Secret,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildEmbeddedIdPConfig builds the embedded IdP configuration.
|
||||||
|
// authStore overrides auth.storage when set.
|
||||||
|
func (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {
|
||||||
|
authStorageType := mgmt.Auth.Storage.Type
|
||||||
|
authStorageDSN := c.Server.AuthStore.DSN
|
||||||
|
if c.Server.AuthStore.Engine != "" {
|
||||||
|
authStorageType = c.Server.AuthStore.Engine
|
||||||
|
}
|
||||||
|
if authStorageType == "" {
|
||||||
|
authStorageType = "sqlite3"
|
||||||
|
}
|
||||||
|
authStorageFile := ""
|
||||||
|
if authStorageType == "postgres" {
|
||||||
|
if authStorageDSN == "" {
|
||||||
|
return nil, fmt.Errorf("authStore.dsn is required when authStore.engine is postgres")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
authStorageFile = path.Join(mgmt.DataDir, "idp.db")
|
||||||
|
if c.Server.AuthStore.File != "" {
|
||||||
|
authStorageFile = c.Server.AuthStore.File
|
||||||
|
if !filepath.IsAbs(authStorageFile) {
|
||||||
|
authStorageFile = filepath.Join(mgmt.DataDir, authStorageFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &idp.EmbeddedIdPConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Issuer: mgmt.Auth.Issuer,
|
||||||
|
LocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,
|
||||||
|
SignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,
|
||||||
|
Storage: idp.EmbeddedStorageConfig{
|
||||||
|
Type: authStorageType,
|
||||||
|
Config: idp.EmbeddedStorageTypeConfig{
|
||||||
|
File: authStorageFile,
|
||||||
|
DSN: authStorageDSN,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,
|
||||||
|
CLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,
|
||||||
|
}
|
||||||
|
|
||||||
|
if mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != "" {
|
||||||
|
cfg.Owner = &idp.OwnerConfig{
|
||||||
|
Email: mgmt.Auth.Owner.Email,
|
||||||
|
Hash: mgmt.Auth.Owner.Password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ToManagementConfig converts CombinedConfig to management server config
|
// ToManagementConfig converts CombinedConfig to management server config
|
||||||
func (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {
|
func (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {
|
||||||
mgmt := c.Management
|
mgmt := c.Management
|
||||||
@@ -550,19 +622,11 @@ func (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {
|
|||||||
// Build relay config
|
// Build relay config
|
||||||
var relayConfig *nbconfig.Relay
|
var relayConfig *nbconfig.Relay
|
||||||
if len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != "" {
|
if len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != "" {
|
||||||
var ttl time.Duration
|
relay, err := buildRelayConfig(mgmt.Relays)
|
||||||
if mgmt.Relays.CredentialsTTL != "" {
|
if err != nil {
|
||||||
var err error
|
return nil, err
|
||||||
ttl, err = time.ParseDuration(mgmt.Relays.CredentialsTTL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid relay credentials TTL %q: %w", mgmt.Relays.CredentialsTTL, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
relayConfig = &nbconfig.Relay{
|
|
||||||
Addresses: mgmt.Relays.Addresses,
|
|
||||||
CredentialsTTL: util.Duration{Duration: ttl},
|
|
||||||
Secret: mgmt.Relays.Secret,
|
|
||||||
}
|
}
|
||||||
|
relayConfig = relay
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build signal config
|
// Build signal config
|
||||||
@@ -598,36 +662,22 @@ func (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {
|
|||||||
httpConfig := &nbconfig.HttpServerConfig{}
|
httpConfig := &nbconfig.HttpServerConfig{}
|
||||||
|
|
||||||
// Build embedded IDP config (always enabled in combined server)
|
// Build embedded IDP config (always enabled in combined server)
|
||||||
storageFile := mgmt.Auth.Storage.File
|
embeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)
|
||||||
if storageFile == "" {
|
if err != nil {
|
||||||
storageFile = path.Join(mgmt.DataDir, "idp.db")
|
return nil, err
|
||||||
}
|
|
||||||
|
|
||||||
embeddedIdP := &idp.EmbeddedIdPConfig{
|
|
||||||
Enabled: true,
|
|
||||||
Issuer: mgmt.Auth.Issuer,
|
|
||||||
LocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,
|
|
||||||
SignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,
|
|
||||||
Storage: idp.EmbeddedStorageConfig{
|
|
||||||
Type: mgmt.Auth.Storage.Type,
|
|
||||||
Config: idp.EmbeddedStorageTypeConfig{
|
|
||||||
File: storageFile,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
DashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,
|
|
||||||
CLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,
|
|
||||||
}
|
|
||||||
|
|
||||||
if mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != "" {
|
|
||||||
embeddedIdP.Owner = &idp.OwnerConfig{
|
|
||||||
Email: mgmt.Auth.Owner.Email,
|
|
||||||
Hash: mgmt.Auth.Owner.Password, // Will be hashed if plain text
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set HTTP config fields for embedded IDP
|
// Set HTTP config fields for embedded IDP
|
||||||
httpConfig.AuthIssuer = mgmt.Auth.Issuer
|
httpConfig.AuthIssuer = mgmt.Auth.Issuer
|
||||||
|
httpConfig.AuthAudience = "netbird-dashboard"
|
||||||
|
httpConfig.AuthClientID = httpConfig.AuthAudience
|
||||||
|
httpConfig.CLIAuthAudience = "netbird-cli"
|
||||||
|
httpConfig.AuthUserIDClaim = "sub"
|
||||||
|
httpConfig.AuthKeysLocation = mgmt.Auth.Issuer + "/keys"
|
||||||
|
httpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + "/.well-known/openid-configuration"
|
||||||
httpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled
|
httpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled
|
||||||
|
callbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, "/oauth2")
|
||||||
|
httpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull
|
||||||
|
|
||||||
return &nbconfig.Config{
|
return &nbconfig.Config{
|
||||||
Stuns: stuns,
|
Stuns: stuns,
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ Configuration is loaded from a YAML file specified with --config.`,
|
|||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to YAML configuration file (required)")
|
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to YAML configuration file (required)")
|
||||||
_ = rootCmd.MarkPersistentFlagRequired("config")
|
_ = rootCmd.MarkPersistentFlagRequired("config")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(newTokenCommands())
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute() error {
|
func Execute() error {
|
||||||
@@ -138,6 +140,23 @@ func initializeConfig() error {
|
|||||||
os.Setenv("NB_STORE_ENGINE_MYSQL_DSN", dsn)
|
os.Setenv("NB_STORE_ENGINE_MYSQL_DSN", dsn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if file := config.Server.Store.File; file != "" {
|
||||||
|
os.Setenv("NB_STORE_ENGINE_SQLITE_FILE", file)
|
||||||
|
}
|
||||||
|
|
||||||
|
if engine := config.Server.ActivityStore.Engine; engine != "" {
|
||||||
|
engineLower := strings.ToLower(engine)
|
||||||
|
if engineLower == "postgres" && config.Server.ActivityStore.DSN == "" {
|
||||||
|
return fmt.Errorf("activityStore.dsn is required when activityStore.engine is postgres")
|
||||||
|
}
|
||||||
|
os.Setenv("NB_ACTIVITY_EVENT_STORE_ENGINE", engineLower)
|
||||||
|
if dsn := config.Server.ActivityStore.DSN; dsn != "" {
|
||||||
|
os.Setenv("NB_ACTIVITY_EVENT_POSTGRES_DSN", dsn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if file := config.Server.ActivityStore.File; file != "" {
|
||||||
|
os.Setenv("NB_ACTIVITY_EVENT_SQLITE_FILE", file)
|
||||||
|
}
|
||||||
|
|
||||||
log.Infof("Starting combined NetBird server")
|
log.Infof("Starting combined NetBird server")
|
||||||
logConfig(config)
|
logConfig(config)
|
||||||
@@ -486,15 +505,17 @@ func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*
|
|||||||
mgmtPort, _ := strconv.Atoi(portStr)
|
mgmtPort, _ := strconv.Atoi(portStr)
|
||||||
|
|
||||||
mgmtSrv := mgmtServer.NewServer(
|
mgmtSrv := mgmtServer.NewServer(
|
||||||
mgmtConfig,
|
&mgmtServer.Config{
|
||||||
dnsDomain,
|
NbConfig: mgmtConfig,
|
||||||
singleAccModeDomain,
|
DNSDomain: dnsDomain,
|
||||||
mgmtPort,
|
MgmtSingleAccModeDomain: singleAccModeDomain,
|
||||||
cfg.Server.MetricsPort,
|
MgmtPort: mgmtPort,
|
||||||
mgmt.DisableAnonymousMetrics,
|
MgmtMetricsPort: cfg.Server.MetricsPort,
|
||||||
mgmt.DisableGeoliteUpdate,
|
DisableMetrics: mgmt.DisableAnonymousMetrics,
|
||||||
// Always enable user deletion from IDP in combined server (embedded IdP is always enabled)
|
DisableGeoliteUpdate: mgmt.DisableGeoliteUpdate,
|
||||||
true,
|
// Always enable user deletion from IDP in combined server (embedded IdP is always enabled)
|
||||||
|
UserDeleteFromIDPEnabled: true,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return mgmtSrv, nil
|
return mgmtSrv, nil
|
||||||
@@ -664,8 +685,11 @@ func logEnvVars() {
|
|||||||
if strings.HasPrefix(env, "NB_") {
|
if strings.HasPrefix(env, "NB_") {
|
||||||
key, _, _ := strings.Cut(env, "=")
|
key, _, _ := strings.Cut(env, "=")
|
||||||
value := os.Getenv(key)
|
value := os.Getenv(key)
|
||||||
if strings.Contains(strings.ToLower(key), "secret") || strings.Contains(strings.ToLower(key), "key") || strings.Contains(strings.ToLower(key), "password") {
|
keyLower := strings.ToLower(key)
|
||||||
|
if strings.Contains(keyLower, "secret") || strings.Contains(keyLower, "key") || strings.Contains(keyLower, "password") {
|
||||||
value = maskSecret(value)
|
value = maskSecret(value)
|
||||||
|
} else if strings.Contains(keyLower, "dsn") {
|
||||||
|
value = maskDSNPassword(value)
|
||||||
}
|
}
|
||||||
log.Infof(" %s=%s", key, value)
|
log.Infof(" %s=%s", key, value)
|
||||||
found = true
|
found = true
|
||||||
|
|||||||
63
combined/cmd/token.go
Normal file
63
combined/cmd/token.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/formatter/hook"
|
||||||
|
tokencmd "github.com/netbirdio/netbird/management/cmd/token"
|
||||||
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newTokenCommands creates the token command tree with combined-specific store opener.
|
||||||
|
func newTokenCommands() *cobra.Command {
|
||||||
|
return tokencmd.NewCommands(withTokenStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// withTokenStore loads the combined YAML config, initializes the store, and calls fn.
|
||||||
|
func withTokenStore(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {
|
||||||
|
if err := util.InitLog("error", "console"); err != nil {
|
||||||
|
return fmt.Errorf("init log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(cmd.Context(), hook.ExecutionContextKey, hook.SystemSource) //nolint:staticcheck
|
||||||
|
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dsn := cfg.Server.Store.DSN; dsn != "" {
|
||||||
|
switch strings.ToLower(cfg.Server.Store.Engine) {
|
||||||
|
case "postgres":
|
||||||
|
os.Setenv("NB_STORE_ENGINE_POSTGRES_DSN", dsn)
|
||||||
|
case "mysql":
|
||||||
|
os.Setenv("NB_STORE_ENGINE_MYSQL_DSN", dsn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if file := cfg.Server.Store.File; file != "" {
|
||||||
|
os.Setenv("NB_STORE_ENGINE_SQLITE_FILE", file)
|
||||||
|
}
|
||||||
|
|
||||||
|
datadir := cfg.Management.DataDir
|
||||||
|
engine := types.Engine(cfg.Management.Store.Engine)
|
||||||
|
|
||||||
|
s, err := store.NewStore(ctx, engine, datadir, nil, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create store: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := s.Close(ctx); err != nil {
|
||||||
|
log.Debugf("close store: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return fn(ctx, s)
|
||||||
|
}
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
# NetBird Combined Server Configuration
|
|
||||||
# Copy this file to config.yaml and customize for your deployment
|
|
||||||
#
|
|
||||||
# This is a Management server with optional embedded Signal, Relay, and STUN services.
|
|
||||||
# By default, all services run locally. You can use external services instead by
|
|
||||||
# setting the corresponding override fields.
|
|
||||||
#
|
|
||||||
# Architecture:
|
|
||||||
# - Management: Always runs locally (this IS the management server)
|
|
||||||
# - Signal: Local by default; set 'signalUri' to use external (disables local)
|
|
||||||
# - Relay: Local by default; set 'relays' to use external (disables local)
|
|
||||||
# - STUN: Local on port 3478 by default; set 'stuns' to use external instead
|
|
||||||
|
|
||||||
server:
|
|
||||||
# Main HTTP/gRPC port for all services (Management, Signal, Relay)
|
|
||||||
listenAddress: ":443"
|
|
||||||
|
|
||||||
# Public address that peers will use to connect to this server
|
|
||||||
# Used for relay connections and management DNS domain
|
|
||||||
# Format: protocol://hostname:port (e.g., https://server.mycompany.com:443)
|
|
||||||
exposedAddress: "https://server.mycompany.com:443"
|
|
||||||
|
|
||||||
# STUN server ports (defaults to [3478] if not specified; set 'stuns' to use external)
|
|
||||||
# stunPorts:
|
|
||||||
# - 3478
|
|
||||||
|
|
||||||
# Metrics endpoint port
|
|
||||||
metricsPort: 9090
|
|
||||||
|
|
||||||
# Healthcheck endpoint address
|
|
||||||
healthcheckAddress: ":9000"
|
|
||||||
|
|
||||||
# Logging configuration
|
|
||||||
logLevel: "info" # Default log level for all components: panic, fatal, error, warn, info, debug, trace
|
|
||||||
logFile: "console" # "console" or path to log file
|
|
||||||
|
|
||||||
# TLS configuration (optional)
|
|
||||||
tls:
|
|
||||||
certFile: ""
|
|
||||||
keyFile: ""
|
|
||||||
letsencrypt:
|
|
||||||
enabled: false
|
|
||||||
dataDir: ""
|
|
||||||
domains: []
|
|
||||||
email: ""
|
|
||||||
awsRoute53: false
|
|
||||||
|
|
||||||
# Shared secret for relay authentication (required when running local relay)
|
|
||||||
authSecret: "your-secret-key-here"
|
|
||||||
|
|
||||||
# Data directory for all services
|
|
||||||
dataDir: "/var/lib/netbird/"
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# External Service Overrides (optional)
|
|
||||||
# Use these to point to external Signal, Relay, or STUN servers instead of
|
|
||||||
# running them locally. When set, the corresponding local service is disabled.
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# External STUN servers - disables local STUN server
|
|
||||||
# stuns:
|
|
||||||
# - uri: "stun:stun.example.com:3478"
|
|
||||||
# - uri: "stun:stun.example.com:3479"
|
|
||||||
|
|
||||||
# External relay servers - disables local relay server
|
|
||||||
# relays:
|
|
||||||
# addresses:
|
|
||||||
# - "rels://relay.example.com:443"
|
|
||||||
# credentialsTTL: "12h"
|
|
||||||
# secret: "relay-shared-secret"
|
|
||||||
|
|
||||||
# External signal server - disables local signal server
|
|
||||||
# signalUri: "https://signal.example.com:443"
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Management Settings
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Metrics and updates
|
|
||||||
disableAnonymousMetrics: false
|
|
||||||
disableGeoliteUpdate: false
|
|
||||||
|
|
||||||
# Embedded authentication/identity provider (Dex) configuration (always enabled)
|
|
||||||
auth:
|
|
||||||
# OIDC issuer URL - must be publicly accessible
|
|
||||||
issuer: "https://server.mycompany.com/oauth2"
|
|
||||||
localAuthDisabled: false
|
|
||||||
signKeyRefreshEnabled: false
|
|
||||||
# OAuth2 redirect URIs for dashboard
|
|
||||||
dashboardRedirectURIs:
|
|
||||||
- "https://app.netbird.io/nb-auth"
|
|
||||||
- "https://app.netbird.io/nb-silent-auth"
|
|
||||||
# OAuth2 redirect URIs for CLI
|
|
||||||
cliRedirectURIs:
|
|
||||||
- "http://localhost:53000/"
|
|
||||||
# Optional initial admin user
|
|
||||||
# owner:
|
|
||||||
# email: "admin@example.com"
|
|
||||||
# password: "initial-password"
|
|
||||||
|
|
||||||
# Store configuration
|
|
||||||
store:
|
|
||||||
engine: "sqlite" # sqlite, postgres, or mysql
|
|
||||||
dsn: "" # Connection string for postgres or mysql
|
|
||||||
encryptionKey: ""
|
|
||||||
|
|
||||||
# Reverse proxy settings (optional)
|
|
||||||
# reverseProxy:
|
|
||||||
# trustedHTTPProxies: []
|
|
||||||
# trustedHTTPProxiesCount: 0
|
|
||||||
# trustedPeers: []
|
|
||||||
@@ -1,11 +1,29 @@
|
|||||||
# Simplified Combined NetBird Server Configuration
|
# NetBird Combined Server Configuration
|
||||||
# Copy this file to config.yaml and customize for your deployment
|
# Copy this file to config.yaml and customize for your deployment
|
||||||
|
#
|
||||||
|
# This is a Management server with optional embedded Signal, Relay, and STUN services.
|
||||||
|
# By default, all services run locally. You can use external services instead by
|
||||||
|
# setting the corresponding override fields.
|
||||||
|
#
|
||||||
|
# Architecture:
|
||||||
|
# - Management: Always runs locally (this IS the management server)
|
||||||
|
# - Signal: Local by default; set 'signalUri' to use external (disables local)
|
||||||
|
# - Relay: Local by default; set 'relays' to use external (disables local)
|
||||||
|
# - STUN: Local on port 3478 by default; set 'stuns' to use external instead
|
||||||
|
|
||||||
# Server-wide settings
|
|
||||||
server:
|
server:
|
||||||
# Main HTTP/gRPC port for all services (Management, Signal, Relay)
|
# Main HTTP/gRPC port for all services (Management, Signal, Relay)
|
||||||
listenAddress: ":443"
|
listenAddress: ":443"
|
||||||
|
|
||||||
|
# Public address that peers will use to connect to this server
|
||||||
|
# Used for relay connections and management DNS domain
|
||||||
|
# Format: protocol://hostname:port (e.g., https://server.mycompany.com:443)
|
||||||
|
exposedAddress: "https://server.mycompany.com:443"
|
||||||
|
|
||||||
|
# STUN server ports (defaults to [3478] if not specified; set 'stuns' to use external)
|
||||||
|
# stunPorts:
|
||||||
|
# - 3478
|
||||||
|
|
||||||
# Metrics endpoint port
|
# Metrics endpoint port
|
||||||
metricsPort: 9090
|
metricsPort: 9090
|
||||||
|
|
||||||
@@ -13,7 +31,7 @@ server:
|
|||||||
healthcheckAddress: ":9000"
|
healthcheckAddress: ":9000"
|
||||||
|
|
||||||
# Logging configuration
|
# Logging configuration
|
||||||
logLevel: "info" # panic, fatal, error, warn, info, debug, trace
|
logLevel: "info" # Default log level for all components: panic, fatal, error, warn, info, debug, trace
|
||||||
logFile: "console" # "console" or path to log file
|
logFile: "console" # "console" or path to log file
|
||||||
|
|
||||||
# TLS configuration (optional)
|
# TLS configuration (optional)
|
||||||
@@ -27,53 +45,45 @@ server:
|
|||||||
email: ""
|
email: ""
|
||||||
awsRoute53: false
|
awsRoute53: false
|
||||||
|
|
||||||
# Relay service configuration
|
# Shared secret for relay authentication (required when running local relay)
|
||||||
relay:
|
|
||||||
# Enable/disable the relay service
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# Public address that peers will use to connect to this relay
|
|
||||||
# Format: hostname:port or ip:port
|
|
||||||
exposedAddress: "relay.example.com:443"
|
|
||||||
|
|
||||||
# Shared secret for relay authentication (required when enabled)
|
|
||||||
authSecret: "your-secret-key-here"
|
authSecret: "your-secret-key-here"
|
||||||
|
|
||||||
# Log level for relay (reserved for future use, currently uses global log level)
|
# Data directory for all services
|
||||||
logLevel: "info"
|
|
||||||
|
|
||||||
# Embedded STUN server (optional)
|
|
||||||
stun:
|
|
||||||
enabled: false
|
|
||||||
ports: [3478]
|
|
||||||
logLevel: "info"
|
|
||||||
|
|
||||||
# Signal service configuration
|
|
||||||
signal:
|
|
||||||
# Enable/disable the signal service
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# Log level for signal (reserved for future use, currently uses global log level)
|
|
||||||
logLevel: "info"
|
|
||||||
|
|
||||||
# Management service configuration
|
|
||||||
management:
|
|
||||||
# Enable/disable the management service
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# Data directory for management service
|
|
||||||
dataDir: "/var/lib/netbird/"
|
dataDir: "/var/lib/netbird/"
|
||||||
|
|
||||||
# DNS domain for the management server
|
# ============================================================================
|
||||||
dnsDomain: ""
|
# External Service Overrides (optional)
|
||||||
|
# Use these to point to external Signal, Relay, or STUN servers instead of
|
||||||
|
# running them locally. When set, the corresponding local service is disabled.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# External STUN servers - disables local STUN server
|
||||||
|
# stuns:
|
||||||
|
# - uri: "stun:stun.example.com:3478"
|
||||||
|
# - uri: "stun:stun.example.com:3479"
|
||||||
|
|
||||||
|
# External relay servers - disables local relay server
|
||||||
|
# relays:
|
||||||
|
# addresses:
|
||||||
|
# - "rels://relay.example.com:443"
|
||||||
|
# credentialsTTL: "12h"
|
||||||
|
# secret: "relay-shared-secret"
|
||||||
|
|
||||||
|
# External signal server - disables local signal server
|
||||||
|
# signalUri: "https://signal.example.com:443"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Management Settings
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
# Metrics and updates
|
# Metrics and updates
|
||||||
disableAnonymousMetrics: false
|
disableAnonymousMetrics: false
|
||||||
disableGeoliteUpdate: false
|
disableGeoliteUpdate: false
|
||||||
|
|
||||||
|
# Embedded authentication/identity provider (Dex) configuration (always enabled)
|
||||||
auth:
|
auth:
|
||||||
# OIDC issuer URL - must be publicly accessible
|
# OIDC issuer URL - must be publicly accessible
|
||||||
issuer: "https://management.example.com/oauth2"
|
issuer: "https://example.com/oauth2"
|
||||||
localAuthDisabled: false
|
localAuthDisabled: false
|
||||||
signKeyRefreshEnabled: false
|
signKeyRefreshEnabled: false
|
||||||
# OAuth2 redirect URIs for dashboard
|
# OAuth2 redirect URIs for dashboard
|
||||||
@@ -88,28 +98,27 @@ management:
|
|||||||
# email: "admin@example.com"
|
# email: "admin@example.com"
|
||||||
# password: "initial-password"
|
# password: "initial-password"
|
||||||
|
|
||||||
# External STUN servers (for client config)
|
|
||||||
stuns: []
|
|
||||||
# - uri: "stun:stun.example.com:3478"
|
|
||||||
|
|
||||||
# External relay servers (for client config)
|
|
||||||
relays:
|
|
||||||
addresses: []
|
|
||||||
# - "rels://relay.example.com:443"
|
|
||||||
credentialsTTL: "12h"
|
|
||||||
secret: ""
|
|
||||||
|
|
||||||
# External signal server URI (for client config)
|
|
||||||
signalUri: ""
|
|
||||||
|
|
||||||
# Store configuration
|
# Store configuration
|
||||||
store:
|
store:
|
||||||
engine: "sqlite" # sqlite, postgres, or mysql
|
engine: "sqlite" # sqlite, postgres, or mysql
|
||||||
dsn: "" # Connection string for postgres or mysql
|
dsn: "" # Connection string for postgres or mysql
|
||||||
encryptionKey: ""
|
encryptionKey: ""
|
||||||
|
# file: "" # Custom SQLite file path (optional, defaults to {dataDir}/store.db)
|
||||||
|
|
||||||
# Reverse proxy settings
|
# Activity events store configuration (optional, defaults to sqlite in dataDir)
|
||||||
reverseProxy:
|
# activityStore:
|
||||||
trustedHTTPProxies: []
|
# engine: "sqlite" # sqlite or postgres
|
||||||
trustedHTTPProxiesCount: 0
|
# dsn: "" # Connection string for postgres
|
||||||
trustedPeers: []
|
# file: "" # Custom SQLite file path (optional, defaults to {dataDir}/events.db)
|
||||||
|
|
||||||
|
# Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db)
|
||||||
|
# authStore:
|
||||||
|
# engine: "sqlite3" # sqlite3 or postgres
|
||||||
|
# dsn: "" # Connection string for postgres (e.g., "host=localhost port=5432 user=postgres password=postgres dbname=netbird_idp sslmode=disable")
|
||||||
|
# file: "" # Custom SQLite file path (optional, defaults to {dataDir}/idp.db)
|
||||||
|
|
||||||
|
# Reverse proxy settings (optional)
|
||||||
|
# reverseProxy:
|
||||||
|
# trustedHTTPProxies: []
|
||||||
|
# trustedHTTPProxiesCount: 0
|
||||||
|
# trustedPeers: []
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package txt
|
package txt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/formatter/levels"
|
"github.com/netbirdio/netbird/formatter/levels"
|
||||||
@@ -18,7 +16,7 @@ type TextFormatter struct {
|
|||||||
func NewTextFormatter() *TextFormatter {
|
func NewTextFormatter() *TextFormatter {
|
||||||
return &TextFormatter{
|
return &TextFormatter{
|
||||||
levelDesc: levels.ValidLevelDesc,
|
levelDesc: levels.ValidLevelDesc,
|
||||||
timestampFormat: time.RFC3339, // or RFC3339
|
timestampFormat: "2006-01-02T15:04:05.000Z07:00",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,6 @@ func TestLogTextFormat(t *testing.T) {
|
|||||||
result, _ := formatter.Format(someEntry)
|
result, _ := formatter.Format(someEntry)
|
||||||
|
|
||||||
parsedString := string(result)
|
parsedString := string(result)
|
||||||
expectedString := "^2021-02-21T01:10:30Z WARN \\[(att1: 1, att2: 2|att2: 2, att1: 1)\\] some/fancy/path.go:46: Some Message\\s+$"
|
expectedString := "^2021-02-21T01:10:30.000Z WARN \\[(att1: 1, att2: 2|att2: 2, att1: 1)\\] some/fancy/path.go:46: Some Message\\s+$"
|
||||||
assert.Regexp(t, expectedString, parsedString)
|
assert.Regexp(t, expectedString, parsedString)
|
||||||
}
|
}
|
||||||
|
|||||||
24
go.mod
24
go.mod
@@ -40,8 +40,9 @@ require (
|
|||||||
github.com/c-robinson/iplib v1.0.3
|
github.com/c-robinson/iplib v1.0.3
|
||||||
github.com/caddyserver/certmagic v0.21.3
|
github.com/caddyserver/certmagic v0.21.3
|
||||||
github.com/cilium/ebpf v0.15.0
|
github.com/cilium/ebpf v0.15.0
|
||||||
github.com/coder/websocket v1.8.13
|
github.com/coder/websocket v1.8.14
|
||||||
github.com/coreos/go-iptables v0.7.0
|
github.com/coreos/go-iptables v0.7.0
|
||||||
|
github.com/coreos/go-oidc/v3 v3.14.1
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
github.com/dexidp/dex v0.0.0-00010101000000-000000000000
|
github.com/dexidp/dex v0.0.0-00010101000000-000000000000
|
||||||
github.com/dexidp/dex/api/v2 v2.4.0
|
github.com/dexidp/dex/api/v2 v2.4.0
|
||||||
@@ -82,6 +83,7 @@ require (
|
|||||||
github.com/pion/stun/v3 v3.1.0
|
github.com/pion/stun/v3 v3.1.0
|
||||||
github.com/pion/transport/v3 v3.1.1
|
github.com/pion/transport/v3 v3.1.1
|
||||||
github.com/pion/turn/v3 v3.0.1
|
github.com/pion/turn/v3 v3.0.1
|
||||||
|
github.com/pires/go-proxyproto v0.11.0
|
||||||
github.com/pkg/sftp v1.13.9
|
github.com/pkg/sftp v1.13.9
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/quic-go/quic-go v0.55.0
|
github.com/quic-go/quic-go v0.55.0
|
||||||
@@ -91,10 +93,10 @@ require (
|
|||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/testcontainers/testcontainers-go v0.31.0
|
github.com/testcontainers/testcontainers-go v0.37.0
|
||||||
github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0
|
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0
|
||||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0
|
github.com/testcontainers/testcontainers-go/modules/postgres v0.37.0
|
||||||
github.com/testcontainers/testcontainers-go/modules/redis v0.31.0
|
github.com/testcontainers/testcontainers-go/modules/redis v0.37.0
|
||||||
github.com/things-go/go-socks5 v0.0.4
|
github.com/things-go/go-socks5 v0.0.4
|
||||||
github.com/ti-mo/conntrack v0.5.1
|
github.com/ti-mo/conntrack v0.5.1
|
||||||
github.com/ti-mo/netfilter v0.5.2
|
github.com/ti-mo/netfilter v0.5.2
|
||||||
@@ -140,7 +142,6 @@ require (
|
|||||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/Microsoft/hcsshim v0.12.3 // indirect
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||||
github.com/awnumar/memcall v0.4.0 // indirect
|
github.com/awnumar/memcall v0.4.0 // indirect
|
||||||
@@ -164,17 +165,16 @@ require (
|
|||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/containerd/containerd v1.7.29 // indirect
|
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/containerd/platforms v0.2.1 // indirect
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1 // indirect
|
|
||||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/docker v26.1.5+incompatible // indirect
|
github.com/docker/docker v28.0.1+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fredbi/uri v1.1.1 // indirect
|
github.com/fredbi/uri v1.1.1 // indirect
|
||||||
github.com/fyne-io/gl-js v0.2.0 // indirect
|
github.com/fyne-io/gl-js v0.2.0 // indirect
|
||||||
@@ -220,9 +220,10 @@ require (
|
|||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/libdns/libdns v0.2.2 // indirect
|
github.com/libdns/libdns v0.2.2 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
|
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.10 // indirect
|
||||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||||
|
github.com/mdelapenya/tlscert v0.2.0 // indirect
|
||||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||||
github.com/mholt/acmez/v2 v2.0.1 // indirect
|
github.com/mholt/acmez/v2 v2.0.1 // indirect
|
||||||
@@ -241,7 +242,7 @@ require (
|
|||||||
github.com/nxadm/tail v1.4.8 // indirect
|
github.com/nxadm/tail v1.4.8 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.10 // indirect
|
github.com/pion/dtls/v2 v2.2.10 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.9 // indirect
|
github.com/pion/dtls/v3 v3.0.9 // indirect
|
||||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||||
@@ -255,6 +256,7 @@ require (
|
|||||||
github.com/prometheus/procfs v0.16.1 // indirect
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
github.com/russellhaering/goxmldsig v1.5.0 // indirect
|
github.com/russellhaering/goxmldsig v1.5.0 // indirect
|
||||||
github.com/rymdport/portal v0.4.2 // indirect
|
github.com/rymdport/portal v0.4.2 // indirect
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/spf13/cast v1.7.0 // indirect
|
github.com/spf13/cast v1.7.0 // indirect
|
||||||
|
|||||||
51
go.sum
51
go.sum
@@ -33,8 +33,6 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe
|
|||||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/Microsoft/hcsshim v0.12.3 h1:LS9NXqXhMoqNCplK1ApmVSfB4UnVLRDWRapB6EIlxE0=
|
|
||||||
github.com/Microsoft/hcsshim v0.12.3/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ=
|
|
||||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||||
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible h1:hqcTK6ZISdip65SR792lwYJTa/axESA0889D3UlZbLo=
|
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible h1:hqcTK6ZISdip65SR792lwYJTa/axESA0889D3UlZbLo=
|
||||||
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible/go.mod h1:6B1nuc1MUs6c62ODZDl7hVE5Pv7O2XGSkgg2olnq34I=
|
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible/go.mod h1:6B1nuc1MUs6c62ODZDl7hVE5Pv7O2XGSkgg2olnq34I=
|
||||||
@@ -107,10 +105,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
|
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
|
||||||
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
|
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
|
||||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||||
github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE=
|
|
||||||
github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
|
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||||
@@ -135,12 +131,14 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
|||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g=
|
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
|
||||||
github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||||
|
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw=
|
github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw=
|
||||||
github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M=
|
github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M=
|
||||||
github.com/eko/gocache/store/go_cache/v4 v4.2.2 h1:tAI9nl6TLoJyKG1ujF0CS0n/IgTEMl+NivxtR5R3/hw=
|
github.com/eko/gocache/store/go_cache/v4 v4.2.2 h1:tAI9nl6TLoJyKG1ujF0CS0n/IgTEMl+NivxtR5R3/hw=
|
||||||
@@ -195,8 +193,6 @@ github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3yg
|
|||||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
|
||||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
@@ -357,13 +353,15 @@ github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81/go.mod h1:RD8ML/Y
|
|||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
|
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
|
||||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
||||||
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
||||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
|
||||||
|
github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
|
||||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||||
@@ -437,13 +435,12 @@ github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
|||||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
|
github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
|
||||||
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
|
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs=
|
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs=
|
||||||
@@ -474,6 +471,8 @@ github.com/pion/turn/v3 v3.0.1 h1:wLi7BTQr6/Q20R0vt/lHbjv6y4GChFtC33nkYbasoT8=
|
|||||||
github.com/pion/turn/v3 v3.0.1/go.mod h1:MrJDKgqryDyWy1/4NT9TWfXWGMC7UHT6pJIv1+gMeNE=
|
github.com/pion/turn/v3 v3.0.1/go.mod h1:MrJDKgqryDyWy1/4NT9TWfXWGMC7UHT6pJIv1+gMeNE=
|
||||||
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
||||||
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
||||||
|
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||||
|
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -511,6 +510,8 @@ github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU
|
|||||||
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
|
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||||
@@ -552,14 +553,14 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U=
|
github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=
|
||||||
github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI=
|
github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=
|
||||||
github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0 h1:790+S8ewZYCbG+o8IiFlZ8ZZ33XbNO6zV9qhU6xhlRk=
|
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0 h1:LqUos1oR5iuuzorFnSvxsHNdYdCHB/DfI82CuT58wbI=
|
||||||
github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0/go.mod h1:REFmO+lSG9S6uSBEwIMZCxeI36uhScjTwChYADeO3JA=
|
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0/go.mod h1:vHEEHx5Kf+uq5hveaVAMrTzPY8eeRZcKcl23MRw5Tkc=
|
||||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 h1:isAwFS3KNKRbJMbWv+wolWqOFUECmjYZ+sIRZCIBc/E=
|
github.com/testcontainers/testcontainers-go/modules/postgres v0.37.0 h1:hsVwFkS6s+79MbKEO+W7A1wNIw1fmkMtF4fg83m6kbc=
|
||||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0/go.mod h1:ZNYY8vumNCEG9YI59A9d6/YaMY49uwRhmeU563EzFGw=
|
github.com/testcontainers/testcontainers-go/modules/postgres v0.37.0/go.mod h1:Qj/eGbRbO/rEYdcRLmN+bEojzatP/+NS1y8ojl2PQsc=
|
||||||
github.com/testcontainers/testcontainers-go/modules/redis v0.31.0 h1:5X6GhOdLwV86zcW8sxppJAMtsDC9u+r9tb3biBc9GKs=
|
github.com/testcontainers/testcontainers-go/modules/redis v0.37.0 h1:9HIY28I9ME/Zmb+zey1p/I1mto5+5ch0wLX+nJdOsQ4=
|
||||||
github.com/testcontainers/testcontainers-go/modules/redis v0.31.0/go.mod h1:dKi5xBwy1k4u8yb3saQHu7hMEJwewHXxzbcMAuLiA6o=
|
github.com/testcontainers/testcontainers-go/modules/redis v0.37.0/go.mod h1:Abu9g/25Qv+FkYVx3U4Voaynou1c+7D0HIhaQJXvk6E=
|
||||||
github.com/things-go/go-socks5 v0.0.4 h1:jMQjIc+qhD4z9cITOMnBiwo9dDmpGuXmBlkRFrl/qD0=
|
github.com/things-go/go-socks5 v0.0.4 h1:jMQjIc+qhD4z9cITOMnBiwo9dDmpGuXmBlkRFrl/qD0=
|
||||||
github.com/things-go/go-socks5 v0.0.4/go.mod h1:sh4K6WHrmHZpjxLTCHyYtXYH8OUuD+yZun41NomR1IQ=
|
github.com/things-go/go-socks5 v0.0.4/go.mod h1:sh4K6WHrmHZpjxLTCHyYtXYH8OUuD+yZun41NomR1IQ=
|
||||||
github.com/ti-mo/conntrack v0.5.1 h1:opEwkFICnDbQc0BUXl73PHBK0h23jEIFVjXsqvF4GY0=
|
github.com/ti-mo/conntrack v0.5.1 h1:opEwkFICnDbQc0BUXl73PHBK0h23jEIFVjXsqvF4GY0=
|
||||||
@@ -849,7 +850,7 @@ gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDa
|
|||||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
|
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||||
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||||
gvisor.dev/gvisor v0.0.0-20251031020517-ecfcdd2f171c h1:pfzmXIkkDgydR4ZRP+e1hXywZfYR21FA0Fbk6ptMkiA=
|
gvisor.dev/gvisor v0.0.0-20251031020517-ecfcdd2f171c h1:pfzmXIkkDgydR4ZRP+e1hXywZfYR21FA0Fbk6ptMkiA=
|
||||||
gvisor.dev/gvisor v0.0.0-20251031020517-ecfcdd2f171c/go.mod h1:/mc6CfwbOm5KKmqoV7Qx20Q+Ja8+vO4g7FuCdlVoAfQ=
|
gvisor.dev/gvisor v0.0.0-20251031020517-ecfcdd2f171c/go.mod h1:/mc6CfwbOm5KKmqoV7Qx20Q+Ja8+vO4g7FuCdlVoAfQ=
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@@ -195,11 +198,175 @@ func (s *Storage) OpenStorage(logger *slog.Logger) (storage.Storage, error) {
|
|||||||
return nil, fmt.Errorf("sqlite3 storage requires 'file' config")
|
return nil, fmt.Errorf("sqlite3 storage requires 'file' config")
|
||||||
}
|
}
|
||||||
return (&sql.SQLite3{File: file}).Open(logger)
|
return (&sql.SQLite3{File: file}).Open(logger)
|
||||||
|
case "postgres":
|
||||||
|
dsn, _ := s.Config["dsn"].(string)
|
||||||
|
if dsn == "" {
|
||||||
|
return nil, fmt.Errorf("postgres storage requires 'dsn' config")
|
||||||
|
}
|
||||||
|
pg, err := parsePostgresDSN(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid postgres DSN: %w", err)
|
||||||
|
}
|
||||||
|
return pg.Open(logger)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported storage type: %s", s.Type)
|
return nil, fmt.Errorf("unsupported storage type: %s", s.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parsePostgresDSN parses a DSN into a sql.Postgres config.
|
||||||
|
// It accepts both URI format (postgres://user:pass@host:port/dbname?sslmode=disable)
|
||||||
|
// and libpq key=value format (host=localhost port=5432 dbname=mydb), including quoted values.
|
||||||
|
func parsePostgresDSN(dsn string) (*sql.Postgres, error) {
|
||||||
|
var params map[string]string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") {
|
||||||
|
params, err = parsePostgresURI(dsn)
|
||||||
|
} else {
|
||||||
|
params, err = parsePostgresKeyValue(dsn)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
host := params["host"]
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
var port uint16 = 5432
|
||||||
|
if p, ok := params["port"]; ok && p != "" {
|
||||||
|
v, err := strconv.ParseUint(p, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid port %q: %w", p, err)
|
||||||
|
}
|
||||||
|
if v == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid port %q: must be non-zero", p)
|
||||||
|
}
|
||||||
|
port = uint16(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbname := params["dbname"]
|
||||||
|
if dbname == "" {
|
||||||
|
return nil, fmt.Errorf("dbname is required in DSN")
|
||||||
|
}
|
||||||
|
|
||||||
|
pg := &sql.Postgres{
|
||||||
|
NetworkDB: sql.NetworkDB{
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
Database: dbname,
|
||||||
|
User: params["user"],
|
||||||
|
Password: params["password"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if sslMode := params["sslmode"]; sslMode != "" {
|
||||||
|
switch sslMode {
|
||||||
|
case "disable", "allow", "prefer", "require", "verify-ca", "verify-full":
|
||||||
|
pg.SSL.Mode = sslMode
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported sslmode %q: valid values are disable, allow, prefer, require, verify-ca, verify-full", sslMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePostgresURI parses a postgres:// or postgresql:// URI into parameter key-value pairs.
|
||||||
|
func parsePostgresURI(dsn string) (map[string]string, error) {
|
||||||
|
u, err := url.Parse(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid postgres URI: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := make(map[string]string)
|
||||||
|
|
||||||
|
if u.User != nil {
|
||||||
|
params["user"] = u.User.Username()
|
||||||
|
if p, ok := u.User.Password(); ok {
|
||||||
|
params["password"] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if u.Hostname() != "" {
|
||||||
|
params["host"] = u.Hostname()
|
||||||
|
}
|
||||||
|
if u.Port() != "" {
|
||||||
|
params["port"] = u.Port()
|
||||||
|
}
|
||||||
|
|
||||||
|
dbname := strings.TrimPrefix(u.Path, "/")
|
||||||
|
if dbname != "" {
|
||||||
|
params["dbname"] = dbname
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range u.Query() {
|
||||||
|
if len(v) > 0 {
|
||||||
|
params[k] = v[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePostgresKeyValue parses a libpq key=value DSN string, handling single-quoted values
|
||||||
|
// (e.g., password='my pass' host=localhost).
|
||||||
|
func parsePostgresKeyValue(dsn string) (map[string]string, error) {
|
||||||
|
params := make(map[string]string)
|
||||||
|
s := strings.TrimSpace(dsn)
|
||||||
|
|
||||||
|
for s != "" {
|
||||||
|
eqIdx := strings.IndexByte(s, '=')
|
||||||
|
if eqIdx < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(s[:eqIdx])
|
||||||
|
|
||||||
|
value, rest, err := parseDSNValue(s[eqIdx+1:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w for key %q", err, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
params[key] = value
|
||||||
|
s = strings.TrimSpace(rest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDSNValue parses the next value from a libpq key=value string positioned after the '='.
|
||||||
|
// It returns the parsed value and the remaining unparsed string.
|
||||||
|
func parseDSNValue(s string) (value, rest string, err error) {
|
||||||
|
if len(s) > 0 && s[0] == '\'' {
|
||||||
|
return parseQuotedDSNValue(s[1:])
|
||||||
|
}
|
||||||
|
// Unquoted value: read until whitespace.
|
||||||
|
idx := strings.IndexAny(s, " \t\n")
|
||||||
|
if idx < 0 {
|
||||||
|
return s, "", nil
|
||||||
|
}
|
||||||
|
return s[:idx], s[idx:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseQuotedDSNValue parses a single-quoted value starting after the opening quote.
|
||||||
|
// Libpq uses ” to represent a literal single quote inside quoted values.
|
||||||
|
func parseQuotedDSNValue(s string) (value, rest string, err error) {
|
||||||
|
var buf strings.Builder
|
||||||
|
for len(s) > 0 {
|
||||||
|
if s[0] == '\'' {
|
||||||
|
if len(s) > 1 && s[1] == '\'' {
|
||||||
|
buf.WriteByte('\'')
|
||||||
|
s = s[2:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return buf.String(), s[1:], nil
|
||||||
|
}
|
||||||
|
buf.WriteByte(s[0])
|
||||||
|
s = s[1:]
|
||||||
|
}
|
||||||
|
return "", "", fmt.Errorf("unterminated quoted value")
|
||||||
|
}
|
||||||
|
|
||||||
// Validate validates the configuration
|
// Validate validates the configuration
|
||||||
func (c *YAMLConfig) Validate() error {
|
func (c *YAMLConfig) Validate() error {
|
||||||
if c.Issuer == "" {
|
if c.Issuer == "" {
|
||||||
|
|||||||
@@ -99,15 +99,16 @@ func NewProvider(ctx context.Context, config *Config) (*Provider, error) {
|
|||||||
|
|
||||||
// Build Dex server config - use Dex's types directly
|
// Build Dex server config - use Dex's types directly
|
||||||
dexConfig := server.Config{
|
dexConfig := server.Config{
|
||||||
Issuer: issuer,
|
Issuer: issuer,
|
||||||
Storage: stor,
|
Storage: stor,
|
||||||
SkipApprovalScreen: true,
|
SkipApprovalScreen: true,
|
||||||
SupportedResponseTypes: []string{"code"},
|
SupportedResponseTypes: []string{"code"},
|
||||||
Logger: logger,
|
ContinueOnConnectorFailure: true,
|
||||||
PrometheusRegistry: prometheus.NewRegistry(),
|
Logger: logger,
|
||||||
RotateKeysAfter: 6 * time.Hour,
|
PrometheusRegistry: prometheus.NewRegistry(),
|
||||||
IDTokensValidFor: 24 * time.Hour,
|
RotateKeysAfter: 6 * time.Hour,
|
||||||
RefreshTokenPolicy: refreshPolicy,
|
IDTokensValidFor: 24 * time.Hour,
|
||||||
|
RefreshTokenPolicy: refreshPolicy,
|
||||||
Web: server.WebConfig{
|
Web: server.WebConfig{
|
||||||
Issuer: "NetBird",
|
Issuer: "NetBird",
|
||||||
},
|
},
|
||||||
@@ -260,6 +261,7 @@ func buildDexConfig(yamlConfig *YAMLConfig, stor storage.Storage, logger *slog.L
|
|||||||
if len(cfg.SupportedResponseTypes) == 0 {
|
if len(cfg.SupportedResponseTypes) == 0 {
|
||||||
cfg.SupportedResponseTypes = []string{"code"}
|
cfg.SupportedResponseTypes = []string{"code"}
|
||||||
}
|
}
|
||||||
|
cfg.ContinueOnConnectorFailure = true
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package dex
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -195,3 +196,64 @@ enablePasswordDB: true
|
|||||||
|
|
||||||
t.Logf("User lookup successful: rawID=%s, connectorID=%s", rawID, connID)
|
t.Logf("User lookup successful: rawID=%s, connectorID=%s", rawID, connID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewProvider_ContinueOnConnectorFailure(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "dex-connector-failure-*")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
Issuer: "http://localhost:5556/dex",
|
||||||
|
Port: 5556,
|
||||||
|
DataDir: tmpDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := NewProvider(ctx, config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = provider.Stop(ctx) }()
|
||||||
|
|
||||||
|
// The provider should have started successfully even though
|
||||||
|
// ContinueOnConnectorFailure is an internal Dex config field.
|
||||||
|
// We verify the provider is functional by performing a basic operation.
|
||||||
|
assert.NotNil(t, provider.dexServer)
|
||||||
|
assert.NotNil(t, provider.storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildDexConfig_ContinueOnConnectorFailure(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "dex-build-config-*")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
yamlContent := `
|
||||||
|
issuer: http://localhost:5556/dex
|
||||||
|
storage:
|
||||||
|
type: sqlite3
|
||||||
|
config:
|
||||||
|
file: ` + filepath.Join(tmpDir, "dex.db") + `
|
||||||
|
web:
|
||||||
|
http: 127.0.0.1:5556
|
||||||
|
enablePasswordDB: true
|
||||||
|
`
|
||||||
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
||||||
|
err = os.WriteFile(configPath, []byte(yamlContent), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
yamlConfig, err := LoadConfig(configPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
stor, err := yamlConfig.Storage.OpenStorage(slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer stor.Close()
|
||||||
|
|
||||||
|
err = initializeStorage(ctx, stor, yamlConfig)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||||
|
cfg := buildDexConfig(yamlConfig, stor, logger)
|
||||||
|
|
||||||
|
assert.True(t, cfg.ContinueOnConnectorFailure,
|
||||||
|
"buildDexConfig must set ContinueOnConnectorFailure to true so management starts even if an external IdP is down")
|
||||||
|
}
|
||||||
|
|||||||
@@ -166,6 +166,74 @@ read_proxy_docker_network() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
read_enable_proxy() {
|
||||||
|
echo "" > /dev/stderr
|
||||||
|
echo "Do you want to enable the NetBird Proxy service?" > /dev/stderr
|
||||||
|
echo "The proxy allows you to selectively expose internal NetBird network resources" > /dev/stderr
|
||||||
|
echo "to the internet. You control which resources are exposed through the dashboard." > /dev/stderr
|
||||||
|
echo -n "Enable proxy? [y/N]: " > /dev/stderr
|
||||||
|
read -r CHOICE < /dev/tty
|
||||||
|
|
||||||
|
if [[ "$CHOICE" =~ ^[Yy]$ ]]; then
|
||||||
|
echo "true"
|
||||||
|
else
|
||||||
|
echo "false"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
read_proxy_domain() {
|
||||||
|
local suggested_proxy="proxy.${BASE_DOMAIN}"
|
||||||
|
|
||||||
|
echo "" > /dev/stderr
|
||||||
|
echo "NOTE: The proxy domain must be different from the management domain ($NETBIRD_DOMAIN)" > /dev/stderr
|
||||||
|
echo "to avoid TLS certificate conflicts." > /dev/stderr
|
||||||
|
echo "" > /dev/stderr
|
||||||
|
echo "You also need to add a wildcard DNS record for the proxy domain," > /dev/stderr
|
||||||
|
echo "e.g. *.${suggested_proxy} pointing to the same server domain as $NETBIRD_DOMAIN with a CNAME record." > /dev/stderr
|
||||||
|
echo "" > /dev/stderr
|
||||||
|
echo -n "Enter the domain for the NetBird Proxy (e.g. ${suggested_proxy}): " > /dev/stderr
|
||||||
|
read -r READ_PROXY_DOMAIN < /dev/tty
|
||||||
|
|
||||||
|
if [[ -z "$READ_PROXY_DOMAIN" ]]; then
|
||||||
|
echo "The proxy domain cannot be empty." > /dev/stderr
|
||||||
|
read_proxy_domain
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$READ_PROXY_DOMAIN" == "$NETBIRD_DOMAIN" ]]; then
|
||||||
|
echo "" > /dev/stderr
|
||||||
|
echo "WARNING: The proxy domain cannot be the same as the management domain ($NETBIRD_DOMAIN)." > /dev/stderr
|
||||||
|
read_proxy_domain
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ${READ_PROXY_DOMAIN} | grep ${NETBIRD_DOMAIN} > /dev/null
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
echo "" > /dev/stderr
|
||||||
|
echo "WARNING: The proxy domain cannot be a subdomain of the management domain ($NETBIRD_DOMAIN)." > /dev/stderr
|
||||||
|
read_proxy_domain
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$READ_PROXY_DOMAIN"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
read_traefik_acme_email() {
|
||||||
|
echo "" > /dev/stderr
|
||||||
|
echo "Enter your email for Let's Encrypt certificate notifications." > /dev/stderr
|
||||||
|
echo -n "Email address: " > /dev/stderr
|
||||||
|
read -r EMAIL < /dev/tty
|
||||||
|
if [[ -z "$EMAIL" ]]; then
|
||||||
|
echo "Email is required for Let's Encrypt." > /dev/stderr
|
||||||
|
read_traefik_acme_email
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "$EMAIL"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
get_bind_address() {
|
get_bind_address() {
|
||||||
if [[ "$BIND_LOCALHOST_ONLY" == "true" ]]; then
|
if [[ "$BIND_LOCALHOST_ONLY" == "true" ]]; then
|
||||||
echo "127.0.0.1"
|
echo "127.0.0.1"
|
||||||
@@ -248,16 +316,26 @@ initialize_default_values() {
|
|||||||
DASHBOARD_IMAGE="netbirdio/dashboard:latest"
|
DASHBOARD_IMAGE="netbirdio/dashboard:latest"
|
||||||
# Combined server replaces separate signal, relay, and management containers
|
# Combined server replaces separate signal, relay, and management containers
|
||||||
NETBIRD_SERVER_IMAGE="netbirdio/netbird-server:latest"
|
NETBIRD_SERVER_IMAGE="netbirdio/netbird-server:latest"
|
||||||
|
NETBIRD_PROXY_IMAGE="netbirdio/reverse-proxy:latest"
|
||||||
|
|
||||||
# Reverse proxy configuration
|
# Reverse proxy configuration
|
||||||
REVERSE_PROXY_TYPE="0"
|
REVERSE_PROXY_TYPE="0"
|
||||||
TRAEFIK_EXTERNAL_NETWORK=""
|
TRAEFIK_EXTERNAL_NETWORK=""
|
||||||
TRAEFIK_ENTRYPOINT="websecure"
|
TRAEFIK_ENTRYPOINT="websecure"
|
||||||
TRAEFIK_CERTRESOLVER=""
|
TRAEFIK_CERTRESOLVER=""
|
||||||
|
TRAEFIK_ACME_EMAIL=""
|
||||||
DASHBOARD_HOST_PORT="8080"
|
DASHBOARD_HOST_PORT="8080"
|
||||||
MANAGEMENT_HOST_PORT="8081" # Combined server port (management + signal + relay)
|
MANAGEMENT_HOST_PORT="8081" # Combined server port (management + signal + relay)
|
||||||
BIND_LOCALHOST_ONLY="true"
|
BIND_LOCALHOST_ONLY="true"
|
||||||
EXTERNAL_PROXY_NETWORK=""
|
EXTERNAL_PROXY_NETWORK=""
|
||||||
|
|
||||||
|
# Traefik static IP within the internal bridge network
|
||||||
|
TRAEFIK_IP="172.30.0.10"
|
||||||
|
|
||||||
|
# NetBird Proxy configuration
|
||||||
|
ENABLE_PROXY="false"
|
||||||
|
PROXY_DOMAIN=""
|
||||||
|
PROXY_TOKEN=""
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,10 +346,12 @@ configure_domain() {
|
|||||||
|
|
||||||
if [[ "$NETBIRD_DOMAIN" == "use-ip" ]]; then
|
if [[ "$NETBIRD_DOMAIN" == "use-ip" ]]; then
|
||||||
NETBIRD_DOMAIN=$(get_main_ip_address)
|
NETBIRD_DOMAIN=$(get_main_ip_address)
|
||||||
|
BASE_DOMAIN=$NETBIRD_DOMAIN
|
||||||
else
|
else
|
||||||
NETBIRD_PORT=443
|
NETBIRD_PORT=443
|
||||||
NETBIRD_HTTP_PROTOCOL="https"
|
NETBIRD_HTTP_PROTOCOL="https"
|
||||||
NETBIRD_RELAY_PROTO="rels"
|
NETBIRD_RELAY_PROTO="rels"
|
||||||
|
BASE_DOMAIN=$(echo $NETBIRD_DOMAIN | sed -E 's/^[^.]+\.//')
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -280,7 +360,16 @@ configure_reverse_proxy() {
|
|||||||
# Prompt for reverse proxy type
|
# Prompt for reverse proxy type
|
||||||
REVERSE_PROXY_TYPE=$(read_reverse_proxy_type)
|
REVERSE_PROXY_TYPE=$(read_reverse_proxy_type)
|
||||||
|
|
||||||
# Handle Traefik-specific prompts (only for external Traefik)
|
# Handle built-in Traefik prompts (option 0)
|
||||||
|
if [[ "$REVERSE_PROXY_TYPE" == "0" ]]; then
|
||||||
|
TRAEFIK_ACME_EMAIL=$(read_traefik_acme_email)
|
||||||
|
ENABLE_PROXY=$(read_enable_proxy)
|
||||||
|
if [[ "$ENABLE_PROXY" == "true" ]]; then
|
||||||
|
PROXY_DOMAIN=$(read_proxy_domain)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle external Traefik-specific prompts (option 1)
|
||||||
if [[ "$REVERSE_PROXY_TYPE" == "1" ]]; then
|
if [[ "$REVERSE_PROXY_TYPE" == "1" ]]; then
|
||||||
TRAEFIK_EXTERNAL_NETWORK=$(read_traefik_network)
|
TRAEFIK_EXTERNAL_NETWORK=$(read_traefik_network)
|
||||||
TRAEFIK_ENTRYPOINT=$(read_traefik_entrypoint)
|
TRAEFIK_ENTRYPOINT=$(read_traefik_entrypoint)
|
||||||
@@ -307,7 +396,7 @@ check_existing_installation() {
|
|||||||
echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
|
echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
|
||||||
echo "You can use the following commands:"
|
echo "You can use the following commands:"
|
||||||
echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
|
echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
|
||||||
echo " rm -f docker-compose.yml dashboard.env config.yaml nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt"
|
echo " rm -f docker-compose.yml dashboard.env config.yaml proxy.env traefik-dynamic.yaml nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt"
|
||||||
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
|
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -321,6 +410,14 @@ generate_configuration_files() {
|
|||||||
case "$REVERSE_PROXY_TYPE" in
|
case "$REVERSE_PROXY_TYPE" in
|
||||||
0)
|
0)
|
||||||
render_docker_compose_traefik_builtin > docker-compose.yml
|
render_docker_compose_traefik_builtin > docker-compose.yml
|
||||||
|
if [[ "$ENABLE_PROXY" == "true" ]]; then
|
||||||
|
# Create placeholder proxy.env so docker-compose can validate
|
||||||
|
# This will be overwritten with the actual token after netbird-server starts
|
||||||
|
echo "# Placeholder - will be updated with token after netbird-server starts" > proxy.env
|
||||||
|
echo "NB_PROXY_TOKEN=placeholder" >> proxy.env
|
||||||
|
# TCP ServersTransport for PROXY protocol v2 to the proxy backend
|
||||||
|
render_traefik_dynamic > traefik-dynamic.yaml
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
1)
|
1)
|
||||||
render_docker_compose_traefik > docker-compose.yml
|
render_docker_compose_traefik > docker-compose.yml
|
||||||
@@ -357,12 +454,45 @@ start_services_and_show_instructions() {
|
|||||||
# For NPM, start containers first (NPM needs services running to create proxy)
|
# For NPM, start containers first (NPM needs services running to create proxy)
|
||||||
# For other external proxies, show instructions first and wait for user confirmation
|
# For other external proxies, show instructions first and wait for user confirmation
|
||||||
if [[ "$REVERSE_PROXY_TYPE" == "0" ]]; then
|
if [[ "$REVERSE_PROXY_TYPE" == "0" ]]; then
|
||||||
# Built-in Traefik - handles everything automatically (TLS via Let's Encrypt)
|
# Built-in Traefik - two-phase startup if proxy is enabled
|
||||||
echo -e "$MSG_STARTING_SERVICES"
|
echo -e "$MSG_STARTING_SERVICES"
|
||||||
$DOCKER_COMPOSE_COMMAND up -d
|
|
||||||
|
|
||||||
sleep 3
|
if [[ "$ENABLE_PROXY" == "true" ]]; then
|
||||||
wait_management_proxy traefik
|
# Phase 1: Start core services (without proxy)
|
||||||
|
echo "Starting core services..."
|
||||||
|
$DOCKER_COMPOSE_COMMAND up -d traefik dashboard netbird-server
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
wait_management_proxy traefik
|
||||||
|
|
||||||
|
# Phase 2: Create proxy token and start proxy
|
||||||
|
echo ""
|
||||||
|
echo "Creating proxy access token..."
|
||||||
|
# Use docker exec with bash to run the token command directly
|
||||||
|
PROXY_TOKEN=$($DOCKER_COMPOSE_COMMAND exec -T netbird-server \
|
||||||
|
/go/bin/netbird-server token create --name "default-proxy" --config /etc/netbird/config.yaml 2>/dev/null | grep "^Token:" | awk '{print $2}')
|
||||||
|
|
||||||
|
if [[ -z "$PROXY_TOKEN" ]]; then
|
||||||
|
echo "ERROR: Failed to create proxy token. Check netbird-server logs." > /dev/stderr
|
||||||
|
$DOCKER_COMPOSE_COMMAND logs --tail=20 netbird-server
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Proxy token created successfully."
|
||||||
|
|
||||||
|
# Generate proxy.env with the token
|
||||||
|
render_proxy_env > proxy.env
|
||||||
|
|
||||||
|
# Start proxy service
|
||||||
|
echo "Starting proxy service..."
|
||||||
|
$DOCKER_COMPOSE_COMMAND up -d proxy
|
||||||
|
else
|
||||||
|
# No proxy - start all services at once
|
||||||
|
$DOCKER_COMPOSE_COMMAND up -d
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
wait_management_proxy traefik
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "$MSG_DONE"
|
echo -e "$MSG_DONE"
|
||||||
print_post_setup_instructions
|
print_post_setup_instructions
|
||||||
@@ -434,6 +564,49 @@ init_environment() {
|
|||||||
############################################
|
############################################
|
||||||
|
|
||||||
render_docker_compose_traefik_builtin() {
|
render_docker_compose_traefik_builtin() {
|
||||||
|
# Generate proxy service section and Traefik dynamic config if enabled
|
||||||
|
local proxy_service=""
|
||||||
|
local proxy_volumes=""
|
||||||
|
local traefik_file_provider=""
|
||||||
|
local traefik_dynamic_volume=""
|
||||||
|
if [[ "$ENABLE_PROXY" == "true" ]]; then
|
||||||
|
traefik_file_provider=' - "--providers.file.filename=/etc/traefik/dynamic.yaml"'
|
||||||
|
traefik_dynamic_volume=" - ./traefik-dynamic.yaml:/etc/traefik/dynamic.yaml:ro"
|
||||||
|
proxy_service="
|
||||||
|
# NetBird Proxy - exposes internal resources to the internet
|
||||||
|
proxy:
|
||||||
|
image: $NETBIRD_PROXY_IMAGE
|
||||||
|
container_name: netbird-proxy
|
||||||
|
ports:
|
||||||
|
- 51820:51820/udp
|
||||||
|
restart: unless-stopped
|
||||||
|
networks: [netbird]
|
||||||
|
depends_on:
|
||||||
|
- netbird-server
|
||||||
|
env_file:
|
||||||
|
- ./proxy.env
|
||||||
|
volumes:
|
||||||
|
- netbird_proxy_certs:/certs
|
||||||
|
labels:
|
||||||
|
# TCP passthrough for any unmatched domain (proxy handles its own TLS)
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.tcp.routers.proxy-passthrough.entrypoints=websecure
|
||||||
|
- traefik.tcp.routers.proxy-passthrough.rule=HostSNI(\`*\`)
|
||||||
|
- traefik.tcp.routers.proxy-passthrough.tls.passthrough=true
|
||||||
|
- traefik.tcp.routers.proxy-passthrough.service=proxy-tls
|
||||||
|
- traefik.tcp.routers.proxy-passthrough.priority=1
|
||||||
|
- traefik.tcp.services.proxy-tls.loadbalancer.server.port=8443
|
||||||
|
- traefik.tcp.services.proxy-tls.loadbalancer.serverstransport=pp-v2@file
|
||||||
|
logging:
|
||||||
|
driver: \"json-file\"
|
||||||
|
options:
|
||||||
|
max-size: \"500m\"
|
||||||
|
max-file: \"2\"
|
||||||
|
"
|
||||||
|
proxy_volumes="
|
||||||
|
netbird_proxy_certs:"
|
||||||
|
fi
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
services:
|
services:
|
||||||
# Traefik reverse proxy (automatic TLS via Let's Encrypt)
|
# Traefik reverse proxy (automatic TLS via Let's Encrypt)
|
||||||
@@ -441,24 +614,43 @@ services:
|
|||||||
image: traefik:v3.6
|
image: traefik:v3.6
|
||||||
container_name: netbird-traefik
|
container_name: netbird-traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks: [netbird]
|
networks:
|
||||||
|
netbird:
|
||||||
|
ipv4_address: $TRAEFIK_IP
|
||||||
command:
|
command:
|
||||||
|
# Logging
|
||||||
|
- "--log.level=INFO"
|
||||||
|
- "--accesslog=true"
|
||||||
|
# Docker provider
|
||||||
- "--providers.docker=true"
|
- "--providers.docker=true"
|
||||||
- "--providers.docker.exposedbydefault=false"
|
- "--providers.docker.exposedbydefault=false"
|
||||||
- "--providers.docker.network=netbird"
|
- "--providers.docker.network=netbird"
|
||||||
|
# Entrypoints
|
||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
- "--entrypoints.websecure.address=:443"
|
- "--entrypoints.websecure.address=:443"
|
||||||
|
- "--entrypoints.websecure.allowACMEByPass=true"
|
||||||
|
# Disable timeouts for long-lived gRPC streams
|
||||||
- "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0"
|
- "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0"
|
||||||
|
- "--entrypoints.websecure.transport.respondingTimeouts.writeTimeout=0"
|
||||||
|
- "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=0"
|
||||||
|
# HTTP to HTTPS redirect
|
||||||
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
|
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
|
||||||
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
|
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
|
||||||
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
|
# Let's Encrypt ACME
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.email=$TRAEFIK_ACME_EMAIL"
|
||||||
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
|
||||||
|
# gRPC transport settings
|
||||||
|
- "--serverstransport.forwardingtimeouts.responseheadertimeout=0s"
|
||||||
|
- "--serverstransport.forwardingtimeouts.idleconntimeout=0s"
|
||||||
|
$traefik_file_provider
|
||||||
ports:
|
ports:
|
||||||
- '443:443'
|
- '443:443'
|
||||||
- '80:80'
|
- '80:80'
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- netbird_traefik_letsencrypt:/letsencrypt
|
- netbird_traefik_letsencrypt:/letsencrypt
|
||||||
|
$traefik_dynamic_volume
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
@@ -479,8 +671,9 @@ services:
|
|||||||
- traefik.http.routers.netbird-dashboard.entrypoints=websecure
|
- traefik.http.routers.netbird-dashboard.entrypoints=websecure
|
||||||
- traefik.http.routers.netbird-dashboard.tls=true
|
- traefik.http.routers.netbird-dashboard.tls=true
|
||||||
- traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt
|
- traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.netbird-dashboard.service=dashboard
|
||||||
- traefik.http.routers.netbird-dashboard.priority=1
|
- traefik.http.routers.netbird-dashboard.priority=1
|
||||||
- traefik.http.services.netbird-dashboard.loadbalancer.server.port=80
|
- traefik.http.services.dashboard.loadbalancer.server.port=80
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
@@ -507,12 +700,14 @@ services:
|
|||||||
- traefik.http.routers.netbird-grpc.tls=true
|
- traefik.http.routers.netbird-grpc.tls=true
|
||||||
- traefik.http.routers.netbird-grpc.tls.certresolver=letsencrypt
|
- traefik.http.routers.netbird-grpc.tls.certresolver=letsencrypt
|
||||||
- traefik.http.routers.netbird-grpc.service=netbird-server-h2c
|
- traefik.http.routers.netbird-grpc.service=netbird-server-h2c
|
||||||
|
- traefik.http.routers.netbird-grpc.priority=100
|
||||||
# Backend router (relay, WebSocket, API, OAuth2)
|
# Backend router (relay, WebSocket, API, OAuth2)
|
||||||
- traefik.http.routers.netbird-backend.rule=Host(\`$NETBIRD_DOMAIN\`) && (PathPrefix(\`/relay\`) || PathPrefix(\`/ws-proxy/\`) || PathPrefix(\`/api\`) || PathPrefix(\`/oauth2\`))
|
- traefik.http.routers.netbird-backend.rule=Host(\`$NETBIRD_DOMAIN\`) && (PathPrefix(\`/relay\`) || PathPrefix(\`/ws-proxy/\`) || PathPrefix(\`/api\`) || PathPrefix(\`/oauth2\`))
|
||||||
- traefik.http.routers.netbird-backend.entrypoints=websecure
|
- traefik.http.routers.netbird-backend.entrypoints=websecure
|
||||||
- traefik.http.routers.netbird-backend.tls=true
|
- traefik.http.routers.netbird-backend.tls=true
|
||||||
- traefik.http.routers.netbird-backend.tls.certresolver=letsencrypt
|
- traefik.http.routers.netbird-backend.tls.certresolver=letsencrypt
|
||||||
- traefik.http.routers.netbird-backend.service=netbird-server
|
- traefik.http.routers.netbird-backend.service=netbird-server
|
||||||
|
- traefik.http.routers.netbird-backend.priority=100
|
||||||
# Services
|
# Services
|
||||||
- traefik.http.services.netbird-server.loadbalancer.server.port=80
|
- traefik.http.services.netbird-server.loadbalancer.server.port=80
|
||||||
- traefik.http.services.netbird-server-h2c.loadbalancer.server.port=80
|
- traefik.http.services.netbird-server-h2c.loadbalancer.server.port=80
|
||||||
@@ -522,13 +717,18 @@ services:
|
|||||||
options:
|
options:
|
||||||
max-size: "500m"
|
max-size: "500m"
|
||||||
max-file: "2"
|
max-file: "2"
|
||||||
|
${proxy_service}
|
||||||
volumes:
|
volumes:
|
||||||
netbird_data:
|
netbird_data:
|
||||||
netbird_traefik_letsencrypt:
|
netbird_traefik_letsencrypt:${proxy_volumes}
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
netbird:
|
netbird:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.30.0.0/24
|
||||||
|
gateway: 172.30.0.1
|
||||||
EOF
|
EOF
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -560,6 +760,10 @@ server:
|
|||||||
cliRedirectURIs:
|
cliRedirectURIs:
|
||||||
- "http://localhost:53000/"
|
- "http://localhost:53000/"
|
||||||
|
|
||||||
|
reverseProxy:
|
||||||
|
trustedHTTPProxies:
|
||||||
|
- "$TRAEFIK_IP/32"
|
||||||
|
|
||||||
store:
|
store:
|
||||||
engine: "sqlite"
|
engine: "sqlite"
|
||||||
encryptionKey: "$DATASTORE_ENCRYPTION_KEY"
|
encryptionKey: "$DATASTORE_ENCRYPTION_KEY"
|
||||||
@@ -589,6 +793,40 @@ EOF
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render_traefik_dynamic() {
|
||||||
|
cat <<'EOF'
|
||||||
|
tcp:
|
||||||
|
serversTransports:
|
||||||
|
pp-v2:
|
||||||
|
proxyProtocol:
|
||||||
|
version: 2
|
||||||
|
EOF
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
render_proxy_env() {
|
||||||
|
cat <<EOF
|
||||||
|
# NetBird Proxy Configuration
|
||||||
|
NB_PROXY_DEBUG_LOGS=false
|
||||||
|
# Use internal Docker network to connect to management (avoids hairpin NAT issues)
|
||||||
|
NB_PROXY_MANAGEMENT_ADDRESS=http://netbird-server:80
|
||||||
|
# Allow insecure gRPC connection to management (required for internal Docker network)
|
||||||
|
NB_PROXY_ALLOW_INSECURE=true
|
||||||
|
# Public URL where this proxy is reachable (used for cluster registration)
|
||||||
|
NB_PROXY_DOMAIN=$PROXY_DOMAIN
|
||||||
|
NB_PROXY_ADDRESS=:8443
|
||||||
|
NB_PROXY_TOKEN=$PROXY_TOKEN
|
||||||
|
NB_PROXY_CERTIFICATE_DIRECTORY=/certs
|
||||||
|
NB_PROXY_ACME_CERTIFICATES=true
|
||||||
|
NB_PROXY_ACME_CHALLENGE_TYPE=tls-alpn-01
|
||||||
|
NB_PROXY_FORWARDED_PROTO=https
|
||||||
|
# Enable PROXY protocol to preserve client IPs through L4 proxies (Traefik TCP passthrough)
|
||||||
|
NB_PROXY_PROXY_PROTOCOL=true
|
||||||
|
# Trust Traefik's IP for PROXY protocol headers
|
||||||
|
NB_PROXY_TRUSTED_PROXIES=$TRAEFIK_IP
|
||||||
|
EOF
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
render_docker_compose_traefik() {
|
render_docker_compose_traefik() {
|
||||||
local network_name="${TRAEFIK_EXTERNAL_NETWORK:-netbird}"
|
local network_name="${TRAEFIK_EXTERNAL_NETWORK:-netbird}"
|
||||||
@@ -939,11 +1177,36 @@ EOF
|
|||||||
############################################
|
############################################
|
||||||
|
|
||||||
print_builtin_traefik_instructions() {
|
print_builtin_traefik_instructions() {
|
||||||
echo "You can access the NetBird dashboard at $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN"
|
echo ""
|
||||||
|
echo "$MSG_SEPARATOR"
|
||||||
|
echo " NETBIRD SETUP COMPLETE"
|
||||||
|
echo "$MSG_SEPARATOR"
|
||||||
|
echo ""
|
||||||
|
echo "You can access the NetBird dashboard at:"
|
||||||
|
echo " $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN"
|
||||||
|
echo ""
|
||||||
echo "Follow the onboarding steps to set up your NetBird instance."
|
echo "Follow the onboarding steps to set up your NetBird instance."
|
||||||
echo ""
|
echo ""
|
||||||
echo "Traefik is handling TLS certificates automatically via Let's Encrypt."
|
echo "Traefik is handling TLS certificates automatically via Let's Encrypt."
|
||||||
echo "If you see certificate warnings, wait a moment for certificate issuance to complete."
|
echo "If you see certificate warnings, wait a moment for certificate issuance to complete."
|
||||||
|
echo ""
|
||||||
|
echo "Open ports:"
|
||||||
|
echo " - 443/tcp (HTTPS - all NetBird services)"
|
||||||
|
echo " - 80/tcp (HTTP - redirects to HTTPS)"
|
||||||
|
echo " - $NETBIRD_STUN_PORT/udp (STUN - required for NAT traversal)"
|
||||||
|
if [[ "$ENABLE_PROXY" == "true" ]]; then
|
||||||
|
echo " - 51820/udp (WIREGUARD - (optional) for P2P proxy connections)"
|
||||||
|
echo ""
|
||||||
|
echo "NetBird Proxy:"
|
||||||
|
echo " The proxy service is enabled and running."
|
||||||
|
echo " Any domain NOT matching $NETBIRD_DOMAIN will be passed through to the proxy."
|
||||||
|
echo " The proxy handles its own TLS certificates via ACME TLS-ALPN-01 challenge."
|
||||||
|
echo " Point your proxy domain to this server's domain address like in the examples below:"
|
||||||
|
echo ""
|
||||||
|
echo " $PROXY_DOMAIN CNAME $NETBIRD_DOMAIN"
|
||||||
|
echo " *.$PROXY_DOMAIN CNAME $NETBIRD_DOMAIN"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1286
infrastructure_files/migrate.sh
Executable file
1286
infrastructure_files/migrate.sh
Executable file
File diff suppressed because it is too large
Load Diff
17
management/Dockerfile.multistage
Normal file
17
management/Dockerfile.multistage
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM golang:1.25-bookworm AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apt-get update && apt-get install -y gcc libc6-dev && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=1 GOOS=linux go build -ldflags="-s -w" -o netbird-mgmt ./management
|
||||||
|
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
RUN apt update && apt install -y ca-certificates && rm -fr /var/cache/apt
|
||||||
|
ENTRYPOINT [ "/go/bin/netbird-mgmt","management"]
|
||||||
|
CMD ["--log-file", "console"]
|
||||||
|
COPY --from=builder /app/netbird-mgmt /go/bin/netbird-mgmt
|
||||||
@@ -19,6 +19,8 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/formatter/hook"
|
"github.com/netbirdio/netbird/formatter/hook"
|
||||||
"github.com/netbirdio/netbird/management/internals/server"
|
"github.com/netbirdio/netbird/management/internals/server"
|
||||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
@@ -27,11 +29,11 @@ import (
|
|||||||
"github.com/netbirdio/netbird/util/crypt"
|
"github.com/netbirdio/netbird/util/crypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var newServer = func(config *nbconfig.Config, dnsDomain, mgmtSingleAccModeDomain string, mgmtPort int, mgmtMetricsPort int, disableMetrics, disableGeoliteUpdate, userDeleteFromIDPEnabled bool) server.Server {
|
var newServer = func(cfg *server.Config) server.Server {
|
||||||
return server.NewServer(config, dnsDomain, mgmtSingleAccModeDomain, mgmtPort, mgmtMetricsPort, disableMetrics, disableGeoliteUpdate, userDeleteFromIDPEnabled)
|
return server.NewServer(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetNewServer(fn func(config *nbconfig.Config, dnsDomain, mgmtSingleAccModeDomain string, mgmtPort int, mgmtMetricsPort int, disableMetrics, disableGeoliteUpdate, userDeleteFromIDPEnabled bool) server.Server) {
|
func SetNewServer(fn func(*server.Config) server.Server) {
|
||||||
newServer = fn
|
newServer = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +110,17 @@ var (
|
|||||||
mgmtSingleAccModeDomain = ""
|
mgmtSingleAccModeDomain = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := newServer(config, dnsDomain, mgmtSingleAccModeDomain, mgmtPort, mgmtMetricsPort, disableMetrics, disableGeoliteUpdate, userDeleteFromIDPEnabled)
|
srv := newServer(&server.Config{
|
||||||
|
NbConfig: config,
|
||||||
|
DNSDomain: dnsDomain,
|
||||||
|
MgmtSingleAccModeDomain: mgmtSingleAccModeDomain,
|
||||||
|
MgmtPort: mgmtPort,
|
||||||
|
MgmtMetricsPort: mgmtMetricsPort,
|
||||||
|
DisableLegacyManagementPort: disableLegacyManagementPort,
|
||||||
|
DisableMetrics: disableMetrics,
|
||||||
|
DisableGeoliteUpdate: disableGeoliteUpdate,
|
||||||
|
UserDeleteFromIDPEnabled: userDeleteFromIDPEnabled,
|
||||||
|
})
|
||||||
go func() {
|
go func() {
|
||||||
if err := srv.Start(cmd.Context()); err != nil {
|
if err := srv.Start(cmd.Context()); err != nil {
|
||||||
log.Fatalf("Server error: %v", err)
|
log.Fatalf("Server error: %v", err)
|
||||||
@@ -213,11 +225,14 @@ func ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config) error {
|
|||||||
// Set HttpConfig values from EmbeddedIdP
|
// Set HttpConfig values from EmbeddedIdP
|
||||||
cfg.HttpConfig.AuthIssuer = issuer
|
cfg.HttpConfig.AuthIssuer = issuer
|
||||||
cfg.HttpConfig.AuthAudience = "netbird-dashboard"
|
cfg.HttpConfig.AuthAudience = "netbird-dashboard"
|
||||||
|
cfg.HttpConfig.AuthClientID = cfg.HttpConfig.AuthAudience
|
||||||
cfg.HttpConfig.CLIAuthAudience = "netbird-cli"
|
cfg.HttpConfig.CLIAuthAudience = "netbird-cli"
|
||||||
cfg.HttpConfig.AuthUserIDClaim = "sub"
|
cfg.HttpConfig.AuthUserIDClaim = "sub"
|
||||||
cfg.HttpConfig.AuthKeysLocation = issuer + "/keys"
|
cfg.HttpConfig.AuthKeysLocation = issuer + "/keys"
|
||||||
cfg.HttpConfig.OIDCConfigEndpoint = issuer + "/.well-known/openid-configuration"
|
cfg.HttpConfig.OIDCConfigEndpoint = issuer + "/.well-known/openid-configuration"
|
||||||
cfg.HttpConfig.IdpSignKeyRefreshEnabled = true
|
cfg.HttpConfig.IdpSignKeyRefreshEnabled = true
|
||||||
|
callbackURL := strings.TrimSuffix(cfg.HttpConfig.AuthIssuer, "/oauth2")
|
||||||
|
cfg.HttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,21 +16,22 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dnsDomain string
|
dnsDomain string
|
||||||
mgmtDataDir string
|
mgmtDataDir string
|
||||||
logLevel string
|
logLevel string
|
||||||
logFile string
|
logFile string
|
||||||
disableMetrics bool
|
disableMetrics bool
|
||||||
disableSingleAccMode bool
|
disableSingleAccMode bool
|
||||||
disableGeoliteUpdate bool
|
disableGeoliteUpdate bool
|
||||||
idpSignKeyRefreshEnabled bool
|
idpSignKeyRefreshEnabled bool
|
||||||
userDeleteFromIDPEnabled bool
|
userDeleteFromIDPEnabled bool
|
||||||
mgmtPort int
|
mgmtPort int
|
||||||
mgmtMetricsPort int
|
mgmtMetricsPort int
|
||||||
mgmtLetsencryptDomain string
|
disableLegacyManagementPort bool
|
||||||
mgmtSingleAccModeDomain string
|
mgmtLetsencryptDomain string
|
||||||
certFile string
|
mgmtSingleAccModeDomain string
|
||||||
certKey string
|
certFile string
|
||||||
|
certKey string
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "netbird-mgmt",
|
Use: "netbird-mgmt",
|
||||||
@@ -55,6 +56,7 @@ func Execute() error {
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
mgmtCmd.Flags().IntVar(&mgmtPort, "port", 80, "server port to listen on (defaults to 443 if TLS is enabled, 80 otherwise")
|
mgmtCmd.Flags().IntVar(&mgmtPort, "port", 80, "server port to listen on (defaults to 443 if TLS is enabled, 80 otherwise")
|
||||||
|
mgmtCmd.Flags().BoolVar(&disableLegacyManagementPort, "disable-legacy-port", false, "disabling the old legacy port (33073)")
|
||||||
mgmtCmd.Flags().IntVar(&mgmtMetricsPort, "metrics-port", 9090, "metrics endpoint http port. Metrics are accessible under host:metrics-port/metrics")
|
mgmtCmd.Flags().IntVar(&mgmtMetricsPort, "metrics-port", 9090, "metrics endpoint http port. Metrics are accessible under host:metrics-port/metrics")
|
||||||
mgmtCmd.Flags().StringVar(&mgmtDataDir, "datadir", defaultMgmtDataDir, "server data directory location")
|
mgmtCmd.Flags().StringVar(&mgmtDataDir, "datadir", defaultMgmtDataDir, "server data directory location")
|
||||||
mgmtCmd.Flags().StringVar(&nbconfig.MgmtConfigPath, "config", defaultMgmtConfig, "Netbird config file location. Config params specified via command line (e.g. datadir) have a precedence over configuration from this file")
|
mgmtCmd.Flags().StringVar(&nbconfig.MgmtConfigPath, "config", defaultMgmtConfig, "Netbird config file location. Config params specified via command line (e.g. datadir) have a precedence over configuration from this file")
|
||||||
@@ -80,4 +82,8 @@ func init() {
|
|||||||
migrationCmd.AddCommand(upCmd)
|
migrationCmd.AddCommand(upCmd)
|
||||||
|
|
||||||
rootCmd.AddCommand(migrationCmd)
|
rootCmd.AddCommand(migrationCmd)
|
||||||
|
|
||||||
|
tc := newTokenCommands()
|
||||||
|
tc.PersistentFlags().StringVar(&nbconfig.MgmtConfigPath, "config", defaultMgmtConfig, "Netbird config file location")
|
||||||
|
rootCmd.AddCommand(tc)
|
||||||
}
|
}
|
||||||
|
|||||||
55
management/cmd/token.go
Normal file
55
management/cmd/token.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/formatter/hook"
|
||||||
|
tokencmd "github.com/netbirdio/netbird/management/cmd/token"
|
||||||
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tokenDatadir string
|
||||||
|
|
||||||
|
// newTokenCommands creates the token command tree with management-specific store opener.
|
||||||
|
func newTokenCommands() *cobra.Command {
|
||||||
|
cmd := tokencmd.NewCommands(withTokenStore)
|
||||||
|
cmd.PersistentFlags().StringVar(&tokenDatadir, "datadir", "", "Override the data directory from config (where store.db is located)")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// withTokenStore initializes logging, loads config, opens the store, and calls fn.
|
||||||
|
func withTokenStore(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {
|
||||||
|
if err := util.InitLog("error", "console"); err != nil {
|
||||||
|
return fmt.Errorf("init log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(cmd.Context(), hook.ExecutionContextKey, hook.SystemSource) //nolint:staticcheck
|
||||||
|
|
||||||
|
config, err := LoadMgmtConfig(ctx, nbconfig.MgmtConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
datadir := config.Datadir
|
||||||
|
if tokenDatadir != "" {
|
||||||
|
datadir = tokenDatadir
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := store.NewStore(ctx, config.StoreConfig.Engine, datadir, nil, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create store: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := s.Close(ctx); err != nil {
|
||||||
|
log.Debugf("close store: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return fn(ctx, s)
|
||||||
|
}
|
||||||
185
management/cmd/token/token.go
Normal file
185
management/cmd/token/token.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
// Package tokencmd provides reusable cobra commands for managing proxy access tokens.
|
||||||
|
// Both the management and combined binaries use these commands, each providing
|
||||||
|
// their own StoreOpener to handle config loading and store initialization.
|
||||||
|
package tokencmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StoreOpener initializes a store from the command context and calls fn.
|
||||||
|
type StoreOpener func(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error
|
||||||
|
|
||||||
|
// NewCommands creates the token command tree with the given store opener.
|
||||||
|
// Returns the parent "token" command with create, list, and revoke subcommands.
|
||||||
|
func NewCommands(opener StoreOpener) *cobra.Command {
|
||||||
|
var (
|
||||||
|
tokenName string
|
||||||
|
tokenExpireIn string
|
||||||
|
)
|
||||||
|
|
||||||
|
tokenCmd := &cobra.Command{
|
||||||
|
Use: "token",
|
||||||
|
Short: "Manage proxy access tokens",
|
||||||
|
Long: "Commands for creating, listing, and revoking proxy access tokens used by reverse proxy instances to authenticate with the management server.",
|
||||||
|
}
|
||||||
|
|
||||||
|
createCmd := &cobra.Command{
|
||||||
|
Use: "create",
|
||||||
|
Short: "Create a new proxy access token",
|
||||||
|
Long: "Creates a new proxy access token. The plain text token is displayed only once at creation time.",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
return opener(cmd, func(ctx context.Context, s store.Store) error {
|
||||||
|
return runCreate(ctx, s, cmd.OutOrStdout(), tokenName, tokenExpireIn)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
createCmd.Flags().StringVar(&tokenName, "name", "", "Name for the token (required)")
|
||||||
|
createCmd.Flags().StringVar(&tokenExpireIn, "expires-in", "", "Token expiration duration (e.g., 365d, 24h, 30d). Empty means no expiration")
|
||||||
|
if err := createCmd.MarkFlagRequired("name"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listCmd := &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Short: "List all proxy access tokens",
|
||||||
|
Long: "Lists all proxy access tokens with their IDs, names, creation dates, expiration, and revocation status.",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
return opener(cmd, func(ctx context.Context, s store.Store) error {
|
||||||
|
return runList(ctx, s, cmd.OutOrStdout())
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
revokeCmd := &cobra.Command{
|
||||||
|
Use: "revoke [token-id]",
|
||||||
|
Short: "Revoke a proxy access token",
|
||||||
|
Long: "Revokes a proxy access token by its ID. Revoked tokens can no longer be used for authentication.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return opener(cmd, func(ctx context.Context, s store.Store) error {
|
||||||
|
return runRevoke(ctx, s, cmd.OutOrStdout(), args[0])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenCmd.AddCommand(createCmd, listCmd, revokeCmd)
|
||||||
|
return tokenCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCreate(ctx context.Context, s store.Store, w io.Writer, name string, expireIn string) error {
|
||||||
|
expiresIn, err := ParseDuration(expireIn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse expiration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
generated, err := types.CreateNewProxyAccessToken(name, expiresIn, nil, "CLI")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("generate token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.SaveProxyAccessToken(ctx, &generated.ProxyAccessToken); err != nil {
|
||||||
|
return fmt.Errorf("save token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(w, "Token created successfully!")
|
||||||
|
_, _ = fmt.Fprintf(w, "Token: %s\n", generated.PlainToken)
|
||||||
|
_, _ = fmt.Fprintln(w)
|
||||||
|
_, _ = fmt.Fprintln(w, "IMPORTANT: Save this token now. It will not be shown again.")
|
||||||
|
_, _ = fmt.Fprintf(w, "Token ID: %s\n", generated.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runList(ctx context.Context, s store.Store, out io.Writer) error {
|
||||||
|
tokens, err := s.GetAllProxyAccessTokens(ctx, store.LockingStrengthNone)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list tokens: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
_, _ = fmt.Fprintln(out, "No proxy access tokens found.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
|
||||||
|
_, _ = fmt.Fprintln(w, "ID\tNAME\tCREATED\tEXPIRES\tLAST USED\tREVOKED")
|
||||||
|
_, _ = fmt.Fprintln(w, "--\t----\t-------\t-------\t---------\t-------")
|
||||||
|
|
||||||
|
for _, t := range tokens {
|
||||||
|
expires := "never"
|
||||||
|
if t.ExpiresAt != nil {
|
||||||
|
expires = t.ExpiresAt.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUsed := "never"
|
||||||
|
if t.LastUsed != nil {
|
||||||
|
lastUsed = t.LastUsed.Format("2006-01-02 15:04")
|
||||||
|
}
|
||||||
|
|
||||||
|
revoked := "no"
|
||||||
|
if t.Revoked {
|
||||||
|
revoked = "yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||||
|
t.ID,
|
||||||
|
t.Name,
|
||||||
|
t.CreatedAt.Format("2006-01-02"),
|
||||||
|
expires,
|
||||||
|
lastUsed,
|
||||||
|
revoked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Flush()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRevoke(ctx context.Context, s store.Store, w io.Writer, tokenID string) error {
|
||||||
|
if err := s.RevokeProxyAccessToken(ctx, tokenID); err != nil {
|
||||||
|
return fmt.Errorf("revoke token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(w, "Token %s revoked successfully.\n", tokenID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDuration parses a duration string with support for days (e.g., "30d", "365d").
|
||||||
|
// An empty string returns zero duration (no expiration).
|
||||||
|
func ParseDuration(s string) (time.Duration, error) {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s[len(s)-1] == 'd' {
|
||||||
|
d, err := strconv.Atoi(s[:len(s)-1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid day format: %s", s)
|
||||||
|
}
|
||||||
|
if d <= 0 {
|
||||||
|
return 0, fmt.Errorf("duration must be positive: %s", s)
|
||||||
|
}
|
||||||
|
return time.Duration(d) * 24 * time.Hour, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := time.ParseDuration(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if d <= 0 {
|
||||||
|
return 0, fmt.Errorf("duration must be positive: %s", s)
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
101
management/cmd/token/token_test.go
Normal file
101
management/cmd/token/token_test.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package tokencmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseDuration(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected time.Duration
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty string returns zero",
|
||||||
|
input: "",
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "days suffix",
|
||||||
|
input: "30d",
|
||||||
|
expected: 30 * 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one day",
|
||||||
|
input: "1d",
|
||||||
|
expected: 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "365 days",
|
||||||
|
input: "365d",
|
||||||
|
expected: 365 * 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hours via Go duration",
|
||||||
|
input: "24h",
|
||||||
|
expected: 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "minutes via Go duration",
|
||||||
|
input: "30m",
|
||||||
|
expected: 30 * time.Minute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex Go duration",
|
||||||
|
input: "1h30m",
|
||||||
|
expected: 90 * time.Minute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid day format",
|
||||||
|
input: "abcd",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative days",
|
||||||
|
input: "-1d",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero days",
|
||||||
|
input: "0d",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-numeric days",
|
||||||
|
input: "xyzd",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative Go duration",
|
||||||
|
input: "-24h",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero Go duration",
|
||||||
|
input: "0s",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid Go duration",
|
||||||
|
input: "notaduration",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := ParseDuration(tt.input)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,8 @@ type Controller struct {
|
|||||||
|
|
||||||
expNewNetworkMap bool
|
expNewNetworkMap bool
|
||||||
expNewNetworkMapAIDs map[string]struct{}
|
expNewNetworkMapAIDs map[string]struct{}
|
||||||
|
|
||||||
|
compactedNetworkMap bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type bufferUpdate struct {
|
type bufferUpdate struct {
|
||||||
@@ -85,6 +87,12 @@ func NewController(ctx context.Context, store store.Store, metrics telemetry.App
|
|||||||
newNetworkMapBuilder = false
|
newNetworkMapBuilder = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compactedNetworkMap, err := strconv.ParseBool(os.Getenv(types.EnvNewNetworkMapCompacted))
|
||||||
|
if err != nil {
|
||||||
|
log.WithContext(ctx).Warnf("failed to parse %s, using default value false: %v", types.EnvNewNetworkMapCompacted, err)
|
||||||
|
compactedNetworkMap = false
|
||||||
|
}
|
||||||
|
|
||||||
ids := strings.Split(os.Getenv(network_map.EnvNewNetworkMapAccounts), ",")
|
ids := strings.Split(os.Getenv(network_map.EnvNewNetworkMapAccounts), ",")
|
||||||
expIDs := make(map[string]struct{}, len(ids))
|
expIDs := make(map[string]struct{}, len(ids))
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
@@ -108,6 +116,8 @@ func NewController(ctx context.Context, store store.Store, metrics telemetry.App
|
|||||||
holder: types.NewHolder(),
|
holder: types.NewHolder(),
|
||||||
expNewNetworkMap: newNetworkMapBuilder,
|
expNewNetworkMap: newNetworkMapBuilder,
|
||||||
expNewNetworkMapAIDs: expIDs,
|
expNewNetworkMapAIDs: expIDs,
|
||||||
|
|
||||||
|
compactedNetworkMap: compactedNetworkMap,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +184,7 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
semaphore := make(chan struct{}, 10)
|
semaphore := make(chan struct{}, 10)
|
||||||
|
|
||||||
|
account.InjectProxyPolicies(ctx)
|
||||||
dnsCache := &cache.DNSConfigCache{}
|
dnsCache := &cache.DNSConfigCache{}
|
||||||
dnsDomain := c.GetDNSDomain(account.Settings)
|
dnsDomain := c.GetDNSDomain(account.Settings)
|
||||||
peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain)
|
peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain)
|
||||||
@@ -229,9 +240,12 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
|
|||||||
|
|
||||||
var remotePeerNetworkMap *types.NetworkMap
|
var remotePeerNetworkMap *types.NetworkMap
|
||||||
|
|
||||||
if c.experimentalNetworkMap(accountID) {
|
switch {
|
||||||
|
case c.experimentalNetworkMap(accountID):
|
||||||
remotePeerNetworkMap = c.getPeerNetworkMapExp(ctx, p.AccountID, p.ID, approvedPeersMap, peersCustomZone, accountZones, c.accountManagerMetrics)
|
remotePeerNetworkMap = c.getPeerNetworkMapExp(ctx, p.AccountID, p.ID, approvedPeersMap, peersCustomZone, accountZones, c.accountManagerMetrics)
|
||||||
} else {
|
case c.compactedNetworkMap:
|
||||||
|
remotePeerNetworkMap = account.GetPeerNetworkMapFromComponents(ctx, p.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
||||||
|
default:
|
||||||
remotePeerNetworkMap = account.GetPeerNetworkMap(ctx, p.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
remotePeerNetworkMap = account.GetPeerNetworkMap(ctx, p.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +340,7 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
|
|||||||
return fmt.Errorf("failed to get validated peers: %v", err)
|
return fmt.Errorf("failed to get validated peers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
account.InjectProxyPolicies(ctx)
|
||||||
dnsCache := &cache.DNSConfigCache{}
|
dnsCache := &cache.DNSConfigCache{}
|
||||||
dnsDomain := c.GetDNSDomain(account.Settings)
|
dnsDomain := c.GetDNSDomain(account.Settings)
|
||||||
peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain)
|
peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain)
|
||||||
@@ -353,9 +368,12 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
|
|||||||
|
|
||||||
var remotePeerNetworkMap *types.NetworkMap
|
var remotePeerNetworkMap *types.NetworkMap
|
||||||
|
|
||||||
if c.experimentalNetworkMap(accountId) {
|
switch {
|
||||||
|
case c.experimentalNetworkMap(accountId):
|
||||||
remotePeerNetworkMap = c.getPeerNetworkMapExp(ctx, peer.AccountID, peer.ID, approvedPeersMap, peersCustomZone, accountZones, c.accountManagerMetrics)
|
remotePeerNetworkMap = c.getPeerNetworkMapExp(ctx, peer.AccountID, peer.ID, approvedPeersMap, peersCustomZone, accountZones, c.accountManagerMetrics)
|
||||||
} else {
|
case c.compactedNetworkMap:
|
||||||
|
remotePeerNetworkMap = account.GetPeerNetworkMapFromComponents(ctx, peerId, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
||||||
|
default:
|
||||||
remotePeerNetworkMap = account.GetPeerNetworkMap(ctx, peerId, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
remotePeerNetworkMap = account.GetPeerNetworkMap(ctx, peerId, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,6 +459,8 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
account.InjectProxyPolicies(ctx)
|
||||||
|
|
||||||
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, 0, err
|
return nil, nil, nil, 0, err
|
||||||
@@ -475,7 +495,12 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
|
|||||||
} else {
|
} else {
|
||||||
resourcePolicies := account.GetResourcePoliciesMap()
|
resourcePolicies := account.GetResourcePoliciesMap()
|
||||||
routers := account.GetResourceRoutersMap()
|
routers := account.GetResourceRoutersMap()
|
||||||
networkMap = account.GetPeerNetworkMap(ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, account.GetActiveGroupUsers())
|
groupIDToUserIDs := account.GetActiveGroupUsers()
|
||||||
|
if c.compactedNetworkMap {
|
||||||
|
networkMap = account.GetPeerNetworkMapFromComponents(ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
||||||
|
} else {
|
||||||
|
networkMap = account.GetPeerNetworkMap(ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyNetworkMap, ok := proxyNetworkMaps[peer.ID]
|
proxyNetworkMap, ok := proxyNetworkMaps[peer.ID]
|
||||||
@@ -847,9 +872,15 @@ func (c *Controller) GetNetworkMap(ctx context.Context, peerID string) (*types.N
|
|||||||
if c.experimentalNetworkMap(peer.AccountID) {
|
if c.experimentalNetworkMap(peer.AccountID) {
|
||||||
networkMap = c.getPeerNetworkMapExp(ctx, peer.AccountID, peerID, validatedPeers, peersCustomZone, accountZones, nil)
|
networkMap = c.getPeerNetworkMapExp(ctx, peer.AccountID, peerID, validatedPeers, peersCustomZone, accountZones, nil)
|
||||||
} else {
|
} else {
|
||||||
|
account.InjectProxyPolicies(ctx)
|
||||||
resourcePolicies := account.GetResourcePoliciesMap()
|
resourcePolicies := account.GetResourcePoliciesMap()
|
||||||
routers := account.GetResourceRoutersMap()
|
routers := account.GetResourceRoutersMap()
|
||||||
networkMap = account.GetPeerNetworkMap(ctx, peer.ID, peersCustomZone, accountZones, validatedPeers, resourcePolicies, routers, nil, account.GetActiveGroupUsers())
|
groupIDToUserIDs := account.GetActiveGroupUsers()
|
||||||
|
if c.compactedNetworkMap {
|
||||||
|
networkMap = account.GetPeerNetworkMapFromComponents(ctx, peer.ID, peersCustomZone, accountZones, validatedPeers, resourcePolicies, routers, nil, groupIDToUserIDs)
|
||||||
|
} else {
|
||||||
|
networkMap = account.GetPeerNetworkMap(ctx, peer.ID, peersCustomZone, accountZones, validatedPeers, resourcePolicies, routers, nil, groupIDToUserIDs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyNetworkMap, ok := proxyNetworkMaps[peer.ID]
|
proxyNetworkMap, ok := proxyNetworkMaps[peer.ID]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/xid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||||
@@ -32,6 +33,7 @@ type Manager interface {
|
|||||||
SetIntegratedPeerValidator(integratedPeerValidator integrated_validator.IntegratedValidator)
|
SetIntegratedPeerValidator(integratedPeerValidator integrated_validator.IntegratedValidator)
|
||||||
SetAccountManager(accountManager account.Manager)
|
SetAccountManager(accountManager account.Manager)
|
||||||
GetPeerID(ctx context.Context, peerKey string) (string, error)
|
GetPeerID(ctx context.Context, peerKey string) (string, error)
|
||||||
|
CreateProxyPeer(ctx context.Context, accountID string, peerKey string, cluster string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type managerImpl struct {
|
type managerImpl struct {
|
||||||
@@ -182,3 +184,36 @@ func (m *managerImpl) DeletePeers(ctx context.Context, accountID string, peerIDs
|
|||||||
func (m *managerImpl) GetPeerID(ctx context.Context, peerKey string) (string, error) {
|
func (m *managerImpl) GetPeerID(ctx context.Context, peerKey string) (string, error) {
|
||||||
return m.store.GetPeerIDByKey(ctx, store.LockingStrengthNone, peerKey)
|
return m.store.GetPeerIDByKey(ctx, store.LockingStrengthNone, peerKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *managerImpl) CreateProxyPeer(ctx context.Context, accountID string, peerKey string, cluster string) error {
|
||||||
|
existingPeerID, err := m.store.GetPeerIDByKey(ctx, store.LockingStrengthNone, peerKey)
|
||||||
|
if err == nil && existingPeerID != "" {
|
||||||
|
// Peer already exists
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name := fmt.Sprintf("proxy-%s", xid.New().String())
|
||||||
|
peer := &peer.Peer{
|
||||||
|
Ephemeral: true,
|
||||||
|
ProxyMeta: peer.ProxyMeta{
|
||||||
|
Cluster: cluster,
|
||||||
|
Embedded: true,
|
||||||
|
},
|
||||||
|
Name: name,
|
||||||
|
Key: peerKey,
|
||||||
|
LoginExpirationEnabled: false,
|
||||||
|
InactivityExpirationEnabled: false,
|
||||||
|
Meta: peer.PeerSystemMeta{
|
||||||
|
Hostname: name,
|
||||||
|
GoOS: "proxy",
|
||||||
|
OS: "proxy",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, err = m.accountManager.AddPeer(ctx, accountID, "", "", peer, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create proxy peer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -162,3 +162,17 @@ func (mr *MockManagerMockRecorder) SetNetworkMapController(networkMapController
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNetworkMapController", reflect.TypeOf((*MockManager)(nil).SetNetworkMapController), networkMapController)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNetworkMapController", reflect.TypeOf((*MockManager)(nil).SetNetworkMapController), networkMapController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateProxyPeer mocks base method.
|
||||||
|
func (m *MockManager) CreateProxyPeer(ctx context.Context, accountID string, peerKey string, cluster string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "CreateProxyPeer", ctx, accountID, peerKey, cluster)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProxyPeer indicates an expected call of CreateProxyPeer.
|
||||||
|
func (mr *MockManagerMockRecorder) CreateProxyPeer(ctx, accountID, peerKey, cluster interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateProxyPeer", reflect.TypeOf((*MockManager)(nil).CreateProxyPeer), ctx, accountID, peerKey, cluster)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package accesslogs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccessLogEntry struct {
|
||||||
|
ID string `gorm:"primaryKey"`
|
||||||
|
AccountID string `gorm:"index"`
|
||||||
|
ServiceID string `gorm:"index"`
|
||||||
|
Timestamp time.Time `gorm:"index"`
|
||||||
|
GeoLocation peer.Location `gorm:"embedded;embeddedPrefix:location_"`
|
||||||
|
Method string `gorm:"index"`
|
||||||
|
Host string `gorm:"index"`
|
||||||
|
Path string `gorm:"index"`
|
||||||
|
Duration time.Duration `gorm:"index"`
|
||||||
|
StatusCode int `gorm:"index"`
|
||||||
|
Reason string
|
||||||
|
UserId string `gorm:"index"`
|
||||||
|
AuthMethodUsed string `gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromProto creates an AccessLogEntry from a proto.AccessLog
|
||||||
|
func (a *AccessLogEntry) FromProto(serviceLog *proto.AccessLog) {
|
||||||
|
a.ID = serviceLog.GetLogId()
|
||||||
|
a.ServiceID = serviceLog.GetServiceId()
|
||||||
|
a.Timestamp = serviceLog.GetTimestamp().AsTime()
|
||||||
|
a.Method = serviceLog.GetMethod()
|
||||||
|
a.Host = serviceLog.GetHost()
|
||||||
|
a.Path = serviceLog.GetPath()
|
||||||
|
a.Duration = time.Duration(serviceLog.GetDurationMs()) * time.Millisecond
|
||||||
|
a.StatusCode = int(serviceLog.GetResponseCode())
|
||||||
|
a.UserId = serviceLog.GetUserId()
|
||||||
|
a.AuthMethodUsed = serviceLog.GetAuthMechanism()
|
||||||
|
a.AccountID = serviceLog.GetAccountId()
|
||||||
|
|
||||||
|
if sourceIP := serviceLog.GetSourceIp(); sourceIP != "" {
|
||||||
|
if ip, err := netip.ParseAddr(sourceIP); err == nil {
|
||||||
|
a.GeoLocation.ConnectionIP = net.IP(ip.AsSlice())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !serviceLog.GetAuthSuccess() {
|
||||||
|
a.Reason = "Authentication failed"
|
||||||
|
} else if serviceLog.GetResponseCode() >= 400 {
|
||||||
|
a.Reason = "Request failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToAPIResponse converts an AccessLogEntry to the API ProxyAccessLog type
|
||||||
|
func (a *AccessLogEntry) ToAPIResponse() *api.ProxyAccessLog {
|
||||||
|
var sourceIP *string
|
||||||
|
if a.GeoLocation.ConnectionIP != nil {
|
||||||
|
ip := a.GeoLocation.ConnectionIP.String()
|
||||||
|
sourceIP = &ip
|
||||||
|
}
|
||||||
|
|
||||||
|
var reason *string
|
||||||
|
if a.Reason != "" {
|
||||||
|
reason = &a.Reason
|
||||||
|
}
|
||||||
|
|
||||||
|
var userID *string
|
||||||
|
if a.UserId != "" {
|
||||||
|
userID = &a.UserId
|
||||||
|
}
|
||||||
|
|
||||||
|
var authMethod *string
|
||||||
|
if a.AuthMethodUsed != "" {
|
||||||
|
authMethod = &a.AuthMethodUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
var countryCode *string
|
||||||
|
if a.GeoLocation.CountryCode != "" {
|
||||||
|
countryCode = &a.GeoLocation.CountryCode
|
||||||
|
}
|
||||||
|
|
||||||
|
var cityName *string
|
||||||
|
if a.GeoLocation.CityName != "" {
|
||||||
|
cityName = &a.GeoLocation.CityName
|
||||||
|
}
|
||||||
|
|
||||||
|
return &api.ProxyAccessLog{
|
||||||
|
Id: a.ID,
|
||||||
|
ServiceId: a.ServiceID,
|
||||||
|
Timestamp: a.Timestamp,
|
||||||
|
Method: a.Method,
|
||||||
|
Host: a.Host,
|
||||||
|
Path: a.Path,
|
||||||
|
DurationMs: int(a.Duration.Milliseconds()),
|
||||||
|
StatusCode: a.StatusCode,
|
||||||
|
SourceIp: sourceIP,
|
||||||
|
Reason: reason,
|
||||||
|
UserId: userID,
|
||||||
|
AuthMethodUsed: authMethod,
|
||||||
|
CountryCode: countryCode,
|
||||||
|
CityName: cityName,
|
||||||
|
}
|
||||||
|
}
|
||||||
178
management/internals/modules/reverseproxy/accesslogs/filter.go
Normal file
178
management/internals/modules/reverseproxy/accesslogs/filter.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package accesslogs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultPageSize is the default number of records per page
|
||||||
|
DefaultPageSize = 50
|
||||||
|
// MaxPageSize is the maximum number of records allowed per page
|
||||||
|
MaxPageSize = 100
|
||||||
|
|
||||||
|
// Default sorting
|
||||||
|
DefaultSortBy = "timestamp"
|
||||||
|
DefaultSortOrder = "desc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Valid sortable fields mapped to their database column names or expressions
|
||||||
|
// For multi-column sorts, columns are separated by comma (e.g., "host, path")
|
||||||
|
var validSortFields = map[string]string{
|
||||||
|
"timestamp": "timestamp",
|
||||||
|
"url": "host, path", // Sort by host first, then path
|
||||||
|
"host": "host",
|
||||||
|
"path": "path",
|
||||||
|
"method": "method",
|
||||||
|
"status_code": "status_code",
|
||||||
|
"duration": "duration",
|
||||||
|
"source_ip": "location_connection_ip",
|
||||||
|
"user_id": "user_id",
|
||||||
|
"auth_method": "auth_method_used",
|
||||||
|
"reason": "reason",
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessLogFilter holds pagination, filtering, and sorting parameters for access logs
|
||||||
|
type AccessLogFilter struct {
|
||||||
|
// Page is the current page number (1-indexed)
|
||||||
|
Page int
|
||||||
|
// PageSize is the number of records per page
|
||||||
|
PageSize int
|
||||||
|
|
||||||
|
// Sorting parameters
|
||||||
|
SortBy string // Field to sort by: timestamp, url, host, path, method, status_code, duration, source_ip, user_id, auth_method, reason
|
||||||
|
SortOrder string // Sort order: asc or desc (default: desc)
|
||||||
|
|
||||||
|
// Filtering parameters
|
||||||
|
Search *string // General search across log ID, host, path, source IP, and user fields
|
||||||
|
SourceIP *string // Filter by source IP address
|
||||||
|
Host *string // Filter by host header
|
||||||
|
Path *string // Filter by request path (supports LIKE pattern)
|
||||||
|
UserID *string // Filter by authenticated user ID
|
||||||
|
UserEmail *string // Filter by user email (requires user lookup)
|
||||||
|
UserName *string // Filter by user name (requires user lookup)
|
||||||
|
Method *string // Filter by HTTP method
|
||||||
|
Status *string // Filter by status: "success" (2xx/3xx) or "failed" (1xx/4xx/5xx)
|
||||||
|
StatusCode *int // Filter by HTTP status code
|
||||||
|
StartDate *time.Time // Filter by timestamp >= start_date
|
||||||
|
EndDate *time.Time // Filter by timestamp <= end_date
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFromRequest parses pagination, sorting, and filter parameters from HTTP request query parameters
|
||||||
|
func (f *AccessLogFilter) ParseFromRequest(r *http.Request) {
|
||||||
|
queryParams := r.URL.Query()
|
||||||
|
|
||||||
|
f.Page = parsePositiveInt(queryParams.Get("page"), 1)
|
||||||
|
f.PageSize = min(parsePositiveInt(queryParams.Get("page_size"), DefaultPageSize), MaxPageSize)
|
||||||
|
|
||||||
|
f.SortBy = parseSortField(queryParams.Get("sort_by"))
|
||||||
|
f.SortOrder = parseSortOrder(queryParams.Get("sort_order"))
|
||||||
|
|
||||||
|
f.Search = parseOptionalString(queryParams.Get("search"))
|
||||||
|
f.SourceIP = parseOptionalString(queryParams.Get("source_ip"))
|
||||||
|
f.Host = parseOptionalString(queryParams.Get("host"))
|
||||||
|
f.Path = parseOptionalString(queryParams.Get("path"))
|
||||||
|
f.UserID = parseOptionalString(queryParams.Get("user_id"))
|
||||||
|
f.UserEmail = parseOptionalString(queryParams.Get("user_email"))
|
||||||
|
f.UserName = parseOptionalString(queryParams.Get("user_name"))
|
||||||
|
f.Method = parseOptionalString(queryParams.Get("method"))
|
||||||
|
f.Status = parseOptionalString(queryParams.Get("status"))
|
||||||
|
f.StatusCode = parseOptionalInt(queryParams.Get("status_code"))
|
||||||
|
f.StartDate = parseOptionalRFC3339(queryParams.Get("start_date"))
|
||||||
|
f.EndDate = parseOptionalRFC3339(queryParams.Get("end_date"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePositiveInt parses a positive integer from a string, returning defaultValue if invalid
|
||||||
|
func parsePositiveInt(s string, defaultValue int) int {
|
||||||
|
if s == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
if val, err := strconv.Atoi(s); err == nil && val > 0 {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOptionalString returns a pointer to the string if non-empty, otherwise nil
|
||||||
|
func parseOptionalString(s string) *string {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOptionalInt parses an optional positive integer from a string
|
||||||
|
func parseOptionalInt(s string) *int {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if val, err := strconv.Atoi(s); err == nil && val > 0 {
|
||||||
|
v := val
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOptionalRFC3339 parses an optional RFC3339 timestamp from a string
|
||||||
|
func parseOptionalRFC3339(s string) *time.Time {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOffset calculates the database offset for pagination
|
||||||
|
func (f *AccessLogFilter) GetOffset() int {
|
||||||
|
return (f.Page - 1) * f.PageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLimit returns the page size for database queries
|
||||||
|
func (f *AccessLogFilter) GetLimit() int {
|
||||||
|
return f.PageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSortColumn returns the validated database column name for sorting
|
||||||
|
func (f *AccessLogFilter) GetSortColumn() string {
|
||||||
|
if column, ok := validSortFields[f.SortBy]; ok {
|
||||||
|
return column
|
||||||
|
}
|
||||||
|
return validSortFields[DefaultSortBy]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSortOrder returns the validated sort order (ASC or DESC)
|
||||||
|
func (f *AccessLogFilter) GetSortOrder() string {
|
||||||
|
if f.SortOrder == "asc" || f.SortOrder == "desc" {
|
||||||
|
return f.SortOrder
|
||||||
|
}
|
||||||
|
return DefaultSortOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSortField validates and returns the sort field, defaulting if invalid
|
||||||
|
func parseSortField(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return DefaultSortBy
|
||||||
|
}
|
||||||
|
// Check if the field is valid
|
||||||
|
if _, ok := validSortFields[s]; ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return DefaultSortBy
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSortOrder validates and returns the sort order, defaulting if invalid
|
||||||
|
func parseSortOrder(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return DefaultSortOrder
|
||||||
|
}
|
||||||
|
// Normalize to lowercase
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
if s == "asc" || s == "desc" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return DefaultSortOrder
|
||||||
|
}
|
||||||
@@ -0,0 +1,570 @@
|
|||||||
|
package accesslogs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccessLogFilter_ParseFromRequest(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
queryParams map[string]string
|
||||||
|
expectedPage int
|
||||||
|
expectedPageSize int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default values when no params provided",
|
||||||
|
queryParams: map[string]string{},
|
||||||
|
expectedPage: 1,
|
||||||
|
expectedPageSize: DefaultPageSize,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid page and page_size",
|
||||||
|
queryParams: map[string]string{
|
||||||
|
"page": "2",
|
||||||
|
"page_size": "25",
|
||||||
|
},
|
||||||
|
expectedPage: 2,
|
||||||
|
expectedPageSize: 25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "page_size exceeds max, should cap at MaxPageSize",
|
||||||
|
queryParams: map[string]string{
|
||||||
|
"page": "1",
|
||||||
|
"page_size": "200",
|
||||||
|
},
|
||||||
|
expectedPage: 1,
|
||||||
|
expectedPageSize: MaxPageSize,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid page number, should use default",
|
||||||
|
queryParams: map[string]string{
|
||||||
|
"page": "invalid",
|
||||||
|
"page_size": "10",
|
||||||
|
},
|
||||||
|
expectedPage: 1,
|
||||||
|
expectedPageSize: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid page_size, should use default",
|
||||||
|
queryParams: map[string]string{
|
||||||
|
"page": "2",
|
||||||
|
"page_size": "invalid",
|
||||||
|
},
|
||||||
|
expectedPage: 2,
|
||||||
|
expectedPageSize: DefaultPageSize,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero page number, should use default",
|
||||||
|
queryParams: map[string]string{
|
||||||
|
"page": "0",
|
||||||
|
"page_size": "10",
|
||||||
|
},
|
||||||
|
expectedPage: 1,
|
||||||
|
expectedPageSize: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative page number, should use default",
|
||||||
|
queryParams: map[string]string{
|
||||||
|
"page": "-1",
|
||||||
|
"page_size": "10",
|
||||||
|
},
|
||||||
|
expectedPage: 1,
|
||||||
|
expectedPageSize: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero page_size, should use default",
|
||||||
|
queryParams: map[string]string{
|
||||||
|
"page": "1",
|
||||||
|
"page_size": "0",
|
||||||
|
},
|
||||||
|
expectedPage: 1,
|
||||||
|
expectedPageSize: DefaultPageSize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
q := req.URL.Query()
|
||||||
|
for key, value := range tt.queryParams {
|
||||||
|
q.Set(key, value)
|
||||||
|
}
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
filter := &AccessLogFilter{}
|
||||||
|
filter.ParseFromRequest(req)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedPage, filter.Page, "Page mismatch")
|
||||||
|
assert.Equal(t, tt.expectedPageSize, filter.PageSize, "PageSize mismatch")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLogFilter_GetOffset(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
page int
|
||||||
|
pageSize int
|
||||||
|
expectedOffset int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "first page",
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
expectedOffset: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "second page",
|
||||||
|
page: 2,
|
||||||
|
pageSize: 50,
|
||||||
|
expectedOffset: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "third page with page size 25",
|
||||||
|
page: 3,
|
||||||
|
pageSize: 25,
|
||||||
|
expectedOffset: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "page 10 with page size 10",
|
||||||
|
page: 10,
|
||||||
|
pageSize: 10,
|
||||||
|
expectedOffset: 90,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
filter := &AccessLogFilter{
|
||||||
|
Page: tt.page,
|
||||||
|
PageSize: tt.pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := filter.GetOffset()
|
||||||
|
assert.Equal(t, tt.expectedOffset, offset)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLogFilter_GetLimit(t *testing.T) {
|
||||||
|
filter := &AccessLogFilter{
|
||||||
|
Page: 2,
|
||||||
|
PageSize: 25,
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := filter.GetLimit()
|
||||||
|
assert.Equal(t, 25, limit, "GetLimit should return PageSize")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLogFilter_ParseFromRequest_FilterParams(t *testing.T) {
|
||||||
|
startDate := "2024-01-15T10:30:00Z"
|
||||||
|
endDate := "2024-01-16T15:45:00Z"
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Set("search", "test query")
|
||||||
|
q.Set("source_ip", "192.168.1.1")
|
||||||
|
q.Set("host", "example.com")
|
||||||
|
q.Set("path", "/api/users")
|
||||||
|
q.Set("user_id", "user123")
|
||||||
|
q.Set("user_email", "user@example.com")
|
||||||
|
q.Set("user_name", "John Doe")
|
||||||
|
q.Set("method", "GET")
|
||||||
|
q.Set("status", "success")
|
||||||
|
q.Set("status_code", "200")
|
||||||
|
q.Set("start_date", startDate)
|
||||||
|
q.Set("end_date", endDate)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
filter := &AccessLogFilter{}
|
||||||
|
filter.ParseFromRequest(req)
|
||||||
|
|
||||||
|
require.NotNil(t, filter.Search)
|
||||||
|
assert.Equal(t, "test query", *filter.Search)
|
||||||
|
|
||||||
|
require.NotNil(t, filter.SourceIP)
|
||||||
|
assert.Equal(t, "192.168.1.1", *filter.SourceIP)
|
||||||
|
|
||||||
|
require.NotNil(t, filter.Host)
|
||||||
|
assert.Equal(t, "example.com", *filter.Host)
|
||||||
|
|
||||||
|
require.NotNil(t, filter.Path)
|
||||||
|
assert.Equal(t, "/api/users", *filter.Path)
|
||||||
|
|
||||||
|
require.NotNil(t, filter.UserID)
|
||||||
|
assert.Equal(t, "user123", *filter.UserID)
|
||||||
|
|
||||||
|
require.NotNil(t, filter.UserEmail)
|
||||||
|
assert.Equal(t, "user@example.com", *filter.UserEmail)
|
||||||
|
|
||||||
|
require.NotNil(t, filter.UserName)
|
||||||
|
assert.Equal(t, "John Doe", *filter.UserName)
|
||||||
|
|
||||||
|
require.NotNil(t, filter.Method)
|
||||||
|
assert.Equal(t, "GET", *filter.Method)
|
||||||
|
|
||||||
|
require.NotNil(t, filter.Status)
|
||||||
|
assert.Equal(t, "success", *filter.Status)
|
||||||
|
|
||||||
|
require.NotNil(t, filter.StatusCode)
|
||||||
|
assert.Equal(t, 200, *filter.StatusCode)
|
||||||
|
|
||||||
|
require.NotNil(t, filter.StartDate)
|
||||||
|
expectedStart, _ := time.Parse(time.RFC3339, startDate)
|
||||||
|
assert.Equal(t, expectedStart, *filter.StartDate)
|
||||||
|
|
||||||
|
require.NotNil(t, filter.EndDate)
|
||||||
|
expectedEnd, _ := time.Parse(time.RFC3339, endDate)
|
||||||
|
assert.Equal(t, expectedEnd, *filter.EndDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLogFilter_ParseFromRequest_EmptyFilters(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
|
||||||
|
filter := &AccessLogFilter{}
|
||||||
|
filter.ParseFromRequest(req)
|
||||||
|
|
||||||
|
assert.Nil(t, filter.Search)
|
||||||
|
assert.Nil(t, filter.SourceIP)
|
||||||
|
assert.Nil(t, filter.Host)
|
||||||
|
assert.Nil(t, filter.Path)
|
||||||
|
assert.Nil(t, filter.UserID)
|
||||||
|
assert.Nil(t, filter.UserEmail)
|
||||||
|
assert.Nil(t, filter.UserName)
|
||||||
|
assert.Nil(t, filter.Method)
|
||||||
|
assert.Nil(t, filter.Status)
|
||||||
|
assert.Nil(t, filter.StatusCode)
|
||||||
|
assert.Nil(t, filter.StartDate)
|
||||||
|
assert.Nil(t, filter.EndDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLogFilter_ParseFromRequest_InvalidFilters(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Set("status_code", "invalid")
|
||||||
|
q.Set("start_date", "not-a-date")
|
||||||
|
q.Set("end_date", "2024-99-99")
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
filter := &AccessLogFilter{}
|
||||||
|
filter.ParseFromRequest(req)
|
||||||
|
|
||||||
|
assert.Nil(t, filter.StatusCode, "invalid status_code should be nil")
|
||||||
|
assert.Nil(t, filter.StartDate, "invalid start_date should be nil")
|
||||||
|
assert.Nil(t, filter.EndDate, "invalid end_date should be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePositiveInt(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
defaultValue int
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{"empty string", "", 10, 10},
|
||||||
|
{"valid positive int", "25", 10, 25},
|
||||||
|
{"zero", "0", 10, 10},
|
||||||
|
{"negative", "-5", 10, 10},
|
||||||
|
{"invalid string", "abc", 10, 10},
|
||||||
|
{"float", "3.14", 10, 10},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := parsePositiveInt(tt.input, tt.defaultValue)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseOptionalString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected *string
|
||||||
|
}{
|
||||||
|
{"empty string", "", nil},
|
||||||
|
{"valid string", "hello", strPtr("hello")},
|
||||||
|
{"whitespace", " ", strPtr(" ")},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := parseOptionalString(tt.input)
|
||||||
|
if tt.expected == nil {
|
||||||
|
assert.Nil(t, result)
|
||||||
|
} else {
|
||||||
|
require.NotNil(t, result)
|
||||||
|
assert.Equal(t, *tt.expected, *result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseOptionalInt(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected *int
|
||||||
|
}{
|
||||||
|
{"empty string", "", nil},
|
||||||
|
{"valid positive int", "42", intPtr(42)},
|
||||||
|
{"zero", "0", nil},
|
||||||
|
{"negative", "-10", nil},
|
||||||
|
{"invalid string", "abc", nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := parseOptionalInt(tt.input)
|
||||||
|
if tt.expected == nil {
|
||||||
|
assert.Nil(t, result)
|
||||||
|
} else {
|
||||||
|
require.NotNil(t, result)
|
||||||
|
assert.Equal(t, *tt.expected, *result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseOptionalRFC3339(t *testing.T) {
|
||||||
|
validDate := "2024-01-15T10:30:00Z"
|
||||||
|
expectedTime, _ := time.Parse(time.RFC3339, validDate)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected *time.Time
|
||||||
|
}{
|
||||||
|
{"empty string", "", nil},
|
||||||
|
{"valid RFC3339", validDate, &expectedTime},
|
||||||
|
{"invalid format", "2024-01-15", nil},
|
||||||
|
{"invalid date", "not-a-date", nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := parseOptionalRFC3339(tt.input)
|
||||||
|
if tt.expected == nil {
|
||||||
|
assert.Nil(t, result)
|
||||||
|
} else {
|
||||||
|
require.NotNil(t, result)
|
||||||
|
assert.Equal(t, *tt.expected, *result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLogFilter_SortingDefaults(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
|
||||||
|
filter := &AccessLogFilter{}
|
||||||
|
filter.ParseFromRequest(req)
|
||||||
|
|
||||||
|
assert.Equal(t, DefaultSortBy, filter.SortBy, "SortBy should default to timestamp")
|
||||||
|
assert.Equal(t, DefaultSortOrder, filter.SortOrder, "SortOrder should default to desc")
|
||||||
|
assert.Equal(t, "timestamp", filter.GetSortColumn(), "GetSortColumn should return timestamp")
|
||||||
|
assert.Equal(t, "desc", filter.GetSortOrder(), "GetSortOrder should return desc")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLogFilter_ValidSortFields(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sortBy string
|
||||||
|
expectedColumn string
|
||||||
|
expectedSortByVal string
|
||||||
|
}{
|
||||||
|
{"timestamp", "timestamp", "timestamp", "timestamp"},
|
||||||
|
{"url", "url", "host, path", "url"},
|
||||||
|
{"host", "host", "host", "host"},
|
||||||
|
{"path", "path", "path", "path"},
|
||||||
|
{"method", "method", "method", "method"},
|
||||||
|
{"status_code", "status_code", "status_code", "status_code"},
|
||||||
|
{"duration", "duration", "duration", "duration"},
|
||||||
|
{"source_ip", "source_ip", "location_connection_ip", "source_ip"},
|
||||||
|
{"user_id", "user_id", "user_id", "user_id"},
|
||||||
|
{"auth_method", "auth_method", "auth_method_used", "auth_method"},
|
||||||
|
{"reason", "reason", "reason", "reason"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test?sort_by="+tt.sortBy, nil)
|
||||||
|
|
||||||
|
filter := &AccessLogFilter{}
|
||||||
|
filter.ParseFromRequest(req)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedSortByVal, filter.SortBy, "SortBy mismatch")
|
||||||
|
assert.Equal(t, tt.expectedColumn, filter.GetSortColumn(), "GetSortColumn mismatch")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLogFilter_InvalidSortField(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sortBy string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"invalid field", "invalid_field", DefaultSortBy},
|
||||||
|
{"empty field", "", DefaultSortBy},
|
||||||
|
{"malicious input", "timestamp--DROP", DefaultSortBy},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Set("sort_by", tt.sortBy)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
filter := &AccessLogFilter{}
|
||||||
|
filter.ParseFromRequest(req)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, filter.SortBy, "Invalid sort field should default to timestamp")
|
||||||
|
assert.Equal(t, validSortFields[DefaultSortBy], filter.GetSortColumn())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLogFilter_SortOrder(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sortOrder string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"ascending", "asc", "asc"},
|
||||||
|
{"descending", "desc", "desc"},
|
||||||
|
{"uppercase ASC", "ASC", "asc"},
|
||||||
|
{"uppercase DESC", "DESC", "desc"},
|
||||||
|
{"mixed case Asc", "Asc", "asc"},
|
||||||
|
{"invalid order", "invalid", DefaultSortOrder},
|
||||||
|
{"empty order", "", DefaultSortOrder},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test?sort_order="+tt.sortOrder, nil)
|
||||||
|
|
||||||
|
filter := &AccessLogFilter{}
|
||||||
|
filter.ParseFromRequest(req)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, filter.GetSortOrder(), "GetSortOrder mismatch")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLogFilter_CompleteSortingScenarios(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sortBy string
|
||||||
|
sortOrder string
|
||||||
|
expectedColumn string
|
||||||
|
expectedOrder string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "sort by host ascending",
|
||||||
|
sortBy: "host",
|
||||||
|
sortOrder: "asc",
|
||||||
|
expectedColumn: "host",
|
||||||
|
expectedOrder: "asc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sort by duration descending",
|
||||||
|
sortBy: "duration",
|
||||||
|
sortOrder: "desc",
|
||||||
|
expectedColumn: "duration",
|
||||||
|
expectedOrder: "desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sort by status_code ascending",
|
||||||
|
sortBy: "status_code",
|
||||||
|
sortOrder: "asc",
|
||||||
|
expectedColumn: "status_code",
|
||||||
|
expectedOrder: "asc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid sort with valid order",
|
||||||
|
sortBy: "invalid",
|
||||||
|
sortOrder: "asc",
|
||||||
|
expectedColumn: "timestamp",
|
||||||
|
expectedOrder: "asc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid sort with invalid order",
|
||||||
|
sortBy: "method",
|
||||||
|
sortOrder: "invalid",
|
||||||
|
expectedColumn: "method",
|
||||||
|
expectedOrder: DefaultSortOrder,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test?sort_by="+tt.sortBy+"&sort_order="+tt.sortOrder, nil)
|
||||||
|
|
||||||
|
filter := &AccessLogFilter{}
|
||||||
|
filter.ParseFromRequest(req)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedColumn, filter.GetSortColumn())
|
||||||
|
assert.Equal(t, tt.expectedOrder, filter.GetSortOrder())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSortField(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"valid field", "host", "host"},
|
||||||
|
{"empty string", "", DefaultSortBy},
|
||||||
|
{"invalid field", "invalid", DefaultSortBy},
|
||||||
|
{"malicious input", "timestamp--DROP", DefaultSortBy},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := parseSortField(tt.input)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSortOrder(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"asc lowercase", "asc", "asc"},
|
||||||
|
{"desc lowercase", "desc", "desc"},
|
||||||
|
{"ASC uppercase", "ASC", "asc"},
|
||||||
|
{"DESC uppercase", "DESC", "desc"},
|
||||||
|
{"invalid", "invalid", DefaultSortOrder},
|
||||||
|
{"empty", "", DefaultSortOrder},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := parseSortOrder(tt.input)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for creating pointers
|
||||||
|
func strPtr(s string) *string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
func intPtr(i int) *int {
|
||||||
|
return &i
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package accesslogs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager interface {
|
||||||
|
SaveAccessLog(ctx context.Context, proxyLog *AccessLogEntry) error
|
||||||
|
GetAllAccessLogs(ctx context.Context, accountID, userID string, filter *AccessLogFilter) ([]*AccessLogEntry, int64, error)
|
||||||
|
CleanupOldAccessLogs(ctx context.Context, retentionDays int) (int64, error)
|
||||||
|
StartPeriodicCleanup(ctx context.Context, retentionDays, cleanupIntervalHours int)
|
||||||
|
StopPeriodicCleanup()
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
|
||||||
|
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
manager accesslogs.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterEndpoints(router *mux.Router, manager accesslogs.Manager) {
|
||||||
|
h := &handler{
|
||||||
|
manager: manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
router.HandleFunc("/events/proxy", h.getAccessLogs).Methods("GET", "OPTIONS")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) getAccessLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var filter accesslogs.AccessLogFilter
|
||||||
|
filter.ParseFromRequest(r)
|
||||||
|
|
||||||
|
logs, totalCount, err := h.manager.GetAllAccessLogs(r.Context(), userAuth.AccountId, userAuth.UserId, &filter)
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiLogs := make([]api.ProxyAccessLog, 0, len(logs))
|
||||||
|
for _, log := range logs {
|
||||||
|
apiLogs = append(apiLogs, *log.ToAPIResponse())
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &api.ProxyAccessLogsResponse{
|
||||||
|
Data: apiLogs,
|
||||||
|
Page: filter.Page,
|
||||||
|
PageSize: filter.PageSize,
|
||||||
|
TotalRecords: int(totalCount),
|
||||||
|
TotalPages: getTotalPageCount(int(totalCount), filter.PageSize),
|
||||||
|
}
|
||||||
|
|
||||||
|
util.WriteJSONObject(r.Context(), w, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTotalPageCount calculates the total number of pages
|
||||||
|
func getTotalPageCount(totalCount, pageSize int) int {
|
||||||
|
if pageSize <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return (totalCount + pageSize - 1) / pageSize
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user