mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-03 23:56:38 +00:00
Compare commits
140 Commits
debug
...
set-min-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24b3f2d608 | ||
|
|
ddc020d34f | ||
|
|
21ba6ad266 | ||
|
|
228672aed2 | ||
|
|
712341e73d | ||
|
|
12ae7ff54e | ||
|
|
ffe0a11d34 | ||
|
|
4aa13c61dc | ||
|
|
37ad370344 | ||
|
|
703647da1e | ||
|
|
9eff58ae62 | ||
|
|
3844516aa7 | ||
|
|
f591e47404 | ||
|
|
958650cd3d | ||
|
|
287ae81195 | ||
|
|
a4a30744ad | ||
|
|
2fa1433063 | ||
|
|
c91d7808bf | ||
|
|
a145f0b811 | ||
|
|
e436c39a86 | ||
|
|
86352f00fc | ||
|
|
6bd9352da8 | ||
|
|
4db4494d0d | ||
|
|
d1d6875953 | ||
|
|
7944b8e843 | ||
|
|
dcba6a6b7e | ||
|
|
9322a92ee9 | ||
|
|
e5a6f9e965 | ||
|
|
bde334ecfc | ||
|
|
9fae103370 | ||
|
|
ac06f178fe | ||
|
|
60ee31df3e | ||
|
|
9f859a240e | ||
|
|
c7f79085df | ||
|
|
7146fd03d1 | ||
|
|
a52ef1a066 | ||
|
|
382dba4a85 | ||
|
|
3e621b0fef | ||
|
|
6142828a9c | ||
|
|
6dd6992415 | ||
|
|
21586acc16 | ||
|
|
f9be8f829e | ||
|
|
6a1eda1caa | ||
|
|
21eca7e1d1 | ||
|
|
0812cc61b9 | ||
|
|
eb5798e927 | ||
|
|
623fcb0535 | ||
|
|
97bb74f824 | ||
|
|
2147bf75eb | ||
|
|
e40a29ba17 | ||
|
|
ff330e644e | ||
|
|
713e320c4c | ||
|
|
e67fe89adb | ||
|
|
6cfbb1f320 | ||
|
|
c853011a32 | ||
|
|
b50b89ba14 | ||
|
|
d063fbb8b9 | ||
|
|
e5d42bc963 | ||
|
|
8866394eb6 | ||
|
|
17c20b45ce | ||
|
|
7dacd9cb23 | ||
|
|
6285e0d23e | ||
|
|
a4826cfb5f | ||
|
|
a0bf0bdcc0 | ||
|
|
dffce78a8c | ||
|
|
c7e7ad5030 | ||
|
|
5142dc52c1 | ||
|
|
ecb44ff306 | ||
|
|
e4a5fb3e91 | ||
|
|
e52d352a48 | ||
|
|
f9723c9266 | ||
|
|
8efad1d170 | ||
|
|
c6641be94b | ||
|
|
89cf8a55e2 | ||
|
|
00c3b67182 | ||
|
|
9203690033 | ||
|
|
9683da54b0 | ||
|
|
0e48a772ff | ||
|
|
f118d81d32 | ||
|
|
ca12bc6953 | ||
|
|
9810386937 | ||
|
|
f1625b32bd | ||
|
|
0ecd5f2118 | ||
|
|
940d0c48c6 | ||
|
|
56cecf849e | ||
|
|
05c4aa7c2c | ||
|
|
2a5cb16494 | ||
|
|
9db1932664 | ||
|
|
1bbabf70b0 | ||
|
|
aa575d6f44 | ||
|
|
f66bbcc54c | ||
|
|
5dd6a08ea6 | ||
|
|
eb5d0569ae | ||
|
|
52ea2e84e9 | ||
|
|
78fab877c0 | ||
|
|
65a94f695f | ||
|
|
ec543f89fb | ||
|
|
a7d5c52203 | ||
|
|
582bb58714 | ||
|
|
121dfda915 | ||
|
|
a1c5287b7c | ||
|
|
12f442439a | ||
|
|
d9b691b8a5 | ||
|
|
4aee3c9e33 | ||
|
|
44e799c687 | ||
|
|
be78efbd42 | ||
|
|
6886691213 | ||
|
|
b48afd92fd | ||
|
|
39329e12a1 | ||
|
|
20a5afc359 | ||
|
|
6cb697eed6 | ||
|
|
e0bed2b0fb | ||
|
|
30f025e7dd | ||
|
|
b4d7605147 | ||
|
|
08b6e9d647 | ||
|
|
67ce14eaea | ||
|
|
669904cd06 | ||
|
|
4be826450b | ||
|
|
738387f2de | ||
|
|
baf0678ceb | ||
|
|
7fef8f6758 | ||
|
|
6829a64a2d | ||
|
|
cbf500024f | ||
|
|
509e184e10 | ||
|
|
3e88b7c56e | ||
|
|
b952d8693d | ||
|
|
5b46cc8e9c | ||
|
|
a9d06b883f | ||
|
|
5f06b202c3 | ||
|
|
0eb99c266a | ||
|
|
bac95ace18 | ||
|
|
9812de853b | ||
|
|
ad4f0a6fdf | ||
|
|
4c758c6e52 | ||
|
|
ec5095ba6b | ||
|
|
49a54624f8 | ||
|
|
729bcf2b01 | ||
|
|
a0cdb58303 | ||
|
|
39c99781cb | ||
|
|
01f24907c5 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [netbirdio]
|
||||||
6
.github/workflows/golang-test-darwin.yml
vendored
6
.github/workflows/golang-test-darwin.yml
vendored
@@ -21,6 +21,7 @@ jobs:
|
|||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.23.x"
|
go-version: "1.23.x"
|
||||||
|
cache: false
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -28,8 +29,9 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: macos-go-${{ hashFiles('**/go.sum') }}
|
key: macos-gotest-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
|
macos-gotest-
|
||||||
macos-go-
|
macos-go-
|
||||||
|
|
||||||
- name: Install libpcap
|
- name: Install libpcap
|
||||||
@@ -42,4 +44,4 @@ 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 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 ./...
|
run: NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 $(go list ./... | grep -v /management)
|
||||||
|
|||||||
230
.github/workflows/golang-test-linux.yml
vendored
230
.github/workflows/golang-test-linux.yml
vendored
@@ -11,30 +11,115 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build-cache:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.23.x"
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- 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@v4
|
||||||
|
id: cache
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ env.cache }}
|
||||||
|
${{ env.modcache }}
|
||||||
|
key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
||||||
|
|
||||||
|
- name: Install 32-bit libpcap
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
|
||||||
|
|
||||||
|
- name: Build client
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
working-directory: client
|
||||||
|
run: CGO_ENABLED=1 go build .
|
||||||
|
|
||||||
|
- name: Build client 386
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
working-directory: client
|
||||||
|
run: CGO_ENABLED=1 GOARCH=386 go build -o client-386 .
|
||||||
|
|
||||||
|
- name: Build management
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
working-directory: management
|
||||||
|
run: CGO_ENABLED=1 go build .
|
||||||
|
|
||||||
|
- name: Build management 386
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
working-directory: management
|
||||||
|
run: CGO_ENABLED=1 GOARCH=386 go build -o management-386 .
|
||||||
|
|
||||||
|
- name: Build signal
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
working-directory: signal
|
||||||
|
run: CGO_ENABLED=1 go build .
|
||||||
|
|
||||||
|
- name: Build signal 386
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
working-directory: signal
|
||||||
|
run: CGO_ENABLED=1 GOARCH=386 go build -o signal-386 .
|
||||||
|
|
||||||
|
- name: Build relay
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
working-directory: relay
|
||||||
|
run: CGO_ENABLED=1 go build .
|
||||||
|
|
||||||
|
- name: Build relay 386
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
working-directory: relay
|
||||||
|
run: CGO_ENABLED=1 GOARCH=386 go build -o relay-386 .
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
needs: [build-cache]
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ '386','amd64' ]
|
arch: [ '386','amd64' ]
|
||||||
store: [ 'sqlite', 'postgres']
|
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.23.x"
|
go-version: "1.23.x"
|
||||||
|
cache: false
|
||||||
|
|
||||||
- name: Cache Go modules
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- 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 dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
||||||
|
|
||||||
@@ -49,27 +134,134 @@ 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 }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m -p 1 ./...
|
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v /management)
|
||||||
|
|
||||||
|
test_management:
|
||||||
|
needs: [ build-cache ]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [ '386','amd64' ]
|
||||||
|
store: [ 'sqlite', 'postgres']
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.23.x"
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- 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 dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
||||||
|
|
||||||
|
- name: Install 32-bit libpcap
|
||||||
|
if: matrix.arch == '386'
|
||||||
|
run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
|
||||||
|
|
||||||
|
- 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 }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m -p 1 $(go list ./... | grep /management)
|
||||||
|
|
||||||
|
benchmark:
|
||||||
|
needs: [ build-cache ]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [ '386','amd64' ]
|
||||||
|
store: [ 'sqlite', 'postgres' ]
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.23.x"
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- 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 dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
||||||
|
|
||||||
|
- name: Install 32-bit libpcap
|
||||||
|
if: matrix.arch == '386'
|
||||||
|
run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
|
||||||
|
|
||||||
|
- 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 }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m ./...
|
||||||
|
|
||||||
test_client_on_docker:
|
test_client_on_docker:
|
||||||
|
needs: [ build-cache ]
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.23.x"
|
go-version: "1.23.x"
|
||||||
|
cache: false
|
||||||
- name: Cache Go modules
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- 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 dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
||||||
|
|
||||||
|
|||||||
25
.github/workflows/golang-test-windows.yml
vendored
25
.github/workflows/golang-test-windows.yml
vendored
@@ -24,6 +24,23 @@ jobs:
|
|||||||
id: go
|
id: go
|
||||||
with:
|
with:
|
||||||
go-version: "1.23.x"
|
go-version: "1.23.x"
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Get Go environment
|
||||||
|
run: |
|
||||||
|
echo "cache=$(go env GOCACHE)" >> $env:GITHUB_ENV
|
||||||
|
echo "modcache=$(go env GOMODCACHE)" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ env.cache }}
|
||||||
|
${{ env.modcache }}
|
||||||
|
key: ${{ runner.os }}-gotest-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gotest-
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: Download wintun
|
- name: Download wintun
|
||||||
uses: carlosperate/download-file-action@v2
|
uses: carlosperate/download-file-action@v2
|
||||||
@@ -42,11 +59,13 @@ jobs:
|
|||||||
- run: choco install -y sysinternals --ignore-checksums
|
- run: choco install -y sysinternals --ignore-checksums
|
||||||
- run: choco install -y mingw
|
- run: choco install -y mingw
|
||||||
|
|
||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=C:\Users\runneradmin\go\pkg\mod
|
- 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=C:\Users\runneradmin\AppData\Local\go-build
|
- 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: echo "files=$(go list ./... | ForEach-Object { $_ } | Where-Object { $_ -notmatch '/management' })" >> $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 -timeout 10m -p 1 ./... > 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 -timeout 10m -p 1 ${{ env.files }} > test-out.txt 2>&1"
|
||||||
- name: test output
|
- name: test output
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
run: Get-Content test-out.txt
|
run: Get-Content test-out.txt
|
||||||
|
|||||||
4
.github/workflows/golangci-lint.yml
vendored
4
.github/workflows/golangci-lint.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
|||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v4
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: --timeout=12m
|
args: --timeout=12m --out-format colored-line-number
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.0.16"
|
SIGN_PIPE_VER: "v0.0.17"
|
||||||
GORELEASER_VER: "v2.3.2"
|
GORELEASER_VER: "v2.3.2"
|
||||||
PRODUCT_NAME: "NetBird"
|
PRODUCT_NAME: "NetBird"
|
||||||
COPYRIGHT: "Wiretrustee UG (haftungsbeschreankt)"
|
COPYRIGHT: "Wiretrustee UG (haftungsbeschreankt)"
|
||||||
|
|||||||
@@ -17,9 +17,13 @@
|
|||||||
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
|
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
|
||||||
</a>
|
</a>
|
||||||
<br>
|
<br>
|
||||||
<a href="https://join.slack.com/t/netbirdio/shared_invite/zt-2p5zwhm4g-8fHollzrQa5y4PZF5AEpvQ">
|
<a href="https://join.slack.com/t/netbirdio/shared_invite/zt-2utg2ncdz-W7LEB6toRBLE1Jca37dYpg">
|
||||||
<img src="https://img.shields.io/badge/slack-@netbird-red.svg?logo=slack"/>
|
<img src="https://img.shields.io/badge/slack-@netbird-red.svg?logo=slack"/>
|
||||||
</a>
|
</a>
|
||||||
|
<br>
|
||||||
|
<a href="https://gurubase.io/g/netbird">
|
||||||
|
<img src="https://img.shields.io/badge/Gurubase-Ask%20NetBird%20Guru-006BFF"/>
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,7 +34,7 @@
|
|||||||
<br/>
|
<br/>
|
||||||
See <a href="https://netbird.io/docs/">Documentation</a>
|
See <a href="https://netbird.io/docs/">Documentation</a>
|
||||||
<br/>
|
<br/>
|
||||||
Join our <a href="https://join.slack.com/t/netbirdio/shared_invite/zt-2p5zwhm4g-8fHollzrQa5y4PZF5AEpvQ">Slack channel</a>
|
Join our <a href="https://join.slack.com/t/netbirdio/shared_invite/zt-2utg2ncdz-W7LEB6toRBLE1Jca37dYpg">Slack channel</a>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
</strong>
|
</strong>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.20
|
FROM alpine:3.21.0
|
||||||
RUN apk add --no-cache ca-certificates iptables ip6tables
|
RUN apk add --no-cache ca-certificates iptables ip6tables
|
||||||
ENV NB_FOREGROUND_MODE=true
|
ENV NB_FOREGROUND_MODE=true
|
||||||
ENTRYPOINT [ "/usr/local/bin/netbird","up"]
|
ENTRYPOINT [ "/usr/local/bin/netbird","up"]
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const anonTLD = ".domain"
|
||||||
|
|
||||||
type Anonymizer struct {
|
type Anonymizer struct {
|
||||||
ipAnonymizer map[netip.Addr]netip.Addr
|
ipAnonymizer map[netip.Addr]netip.Addr
|
||||||
domainAnonymizer map[string]string
|
domainAnonymizer map[string]string
|
||||||
@@ -19,6 +21,8 @@ type Anonymizer struct {
|
|||||||
currentAnonIPv6 netip.Addr
|
currentAnonIPv6 netip.Addr
|
||||||
startAnonIPv4 netip.Addr
|
startAnonIPv4 netip.Addr
|
||||||
startAnonIPv6 netip.Addr
|
startAnonIPv6 netip.Addr
|
||||||
|
|
||||||
|
domainKeyRegex *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultAddresses() (netip.Addr, netip.Addr) {
|
func DefaultAddresses() (netip.Addr, netip.Addr) {
|
||||||
@@ -34,6 +38,8 @@ func NewAnonymizer(startIPv4, startIPv6 netip.Addr) *Anonymizer {
|
|||||||
currentAnonIPv6: startIPv6,
|
currentAnonIPv6: startIPv6,
|
||||||
startAnonIPv4: startIPv4,
|
startAnonIPv4: startIPv4,
|
||||||
startAnonIPv6: startIPv6,
|
startAnonIPv6: startIPv6,
|
||||||
|
|
||||||
|
domainKeyRegex: regexp.MustCompile(`\bdomain=([^\s,:"]+)`),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,29 +89,39 @@ func (a *Anonymizer) AnonymizeIPString(ip string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Anonymizer) AnonymizeDomain(domain string) string {
|
func (a *Anonymizer) AnonymizeDomain(domain string) string {
|
||||||
if strings.HasSuffix(domain, "netbird.io") ||
|
baseDomain := domain
|
||||||
strings.HasSuffix(domain, "netbird.selfhosted") ||
|
hasDot := strings.HasSuffix(domain, ".")
|
||||||
strings.HasSuffix(domain, "netbird.cloud") ||
|
if hasDot {
|
||||||
strings.HasSuffix(domain, "netbird.stage") ||
|
baseDomain = domain[:len(domain)-1]
|
||||||
strings.HasSuffix(domain, ".domain") {
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(baseDomain, "netbird.io") ||
|
||||||
|
strings.HasSuffix(baseDomain, "netbird.selfhosted") ||
|
||||||
|
strings.HasSuffix(baseDomain, "netbird.cloud") ||
|
||||||
|
strings.HasSuffix(baseDomain, "netbird.stage") ||
|
||||||
|
strings.HasSuffix(baseDomain, anonTLD) {
|
||||||
return domain
|
return domain
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(domain, ".")
|
parts := strings.Split(baseDomain, ".")
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
return domain
|
return domain
|
||||||
}
|
}
|
||||||
|
|
||||||
baseDomain := parts[len(parts)-2] + "." + parts[len(parts)-1]
|
baseForLookup := parts[len(parts)-2] + "." + parts[len(parts)-1]
|
||||||
|
|
||||||
anonymized, ok := a.domainAnonymizer[baseDomain]
|
anonymized, ok := a.domainAnonymizer[baseForLookup]
|
||||||
if !ok {
|
if !ok {
|
||||||
anonymizedBase := "anon-" + generateRandomString(5) + ".domain"
|
anonymizedBase := "anon-" + generateRandomString(5) + anonTLD
|
||||||
a.domainAnonymizer[baseDomain] = anonymizedBase
|
a.domainAnonymizer[baseForLookup] = anonymizedBase
|
||||||
anonymized = anonymizedBase
|
anonymized = anonymizedBase
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Replace(domain, baseDomain, anonymized, 1)
|
result := strings.Replace(baseDomain, baseForLookup, anonymized, 1)
|
||||||
|
if hasDot {
|
||||||
|
result += "."
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Anonymizer) AnonymizeURI(uri string) string {
|
func (a *Anonymizer) AnonymizeURI(uri string) string {
|
||||||
@@ -152,27 +168,22 @@ func (a *Anonymizer) AnonymizeString(str string) string {
|
|||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnonymizeSchemeURI finds and anonymizes URIs with stun, stuns, turn, and turns schemes.
|
// AnonymizeSchemeURI finds and anonymizes URIs with ws, wss, rel, rels, stun, stuns, turn, and turns schemes.
|
||||||
func (a *Anonymizer) AnonymizeSchemeURI(text string) string {
|
func (a *Anonymizer) AnonymizeSchemeURI(text string) string {
|
||||||
re := regexp.MustCompile(`(?i)\b(stuns?:|turns?:|https?://)\S+\b`)
|
re := regexp.MustCompile(`(?i)\b(wss?://|rels?://|stuns?:|turns?:|https?://)\S+\b`)
|
||||||
|
|
||||||
return re.ReplaceAllStringFunc(text, a.AnonymizeURI)
|
return re.ReplaceAllStringFunc(text, a.AnonymizeURI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnonymizeDNSLogLine anonymizes domain names in DNS log entries by replacing them with a random string.
|
|
||||||
func (a *Anonymizer) AnonymizeDNSLogLine(logEntry string) string {
|
func (a *Anonymizer) AnonymizeDNSLogLine(logEntry string) string {
|
||||||
domainPattern := `dns\.Question{Name:"([^"]+)",`
|
return a.domainKeyRegex.ReplaceAllStringFunc(logEntry, func(match string) string {
|
||||||
domainRegex := regexp.MustCompile(domainPattern)
|
parts := strings.SplitN(match, "=", 2)
|
||||||
|
|
||||||
return domainRegex.ReplaceAllStringFunc(logEntry, func(match string) string {
|
|
||||||
parts := strings.Split(match, `"`)
|
|
||||||
if len(parts) >= 2 {
|
if len(parts) >= 2 {
|
||||||
domain := parts[1]
|
domain := parts[1]
|
||||||
if strings.HasSuffix(domain, ".domain") {
|
if strings.HasSuffix(domain, anonTLD) {
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
randomDomain := generateRandomString(10) + ".domain"
|
return "domain=" + a.AnonymizeDomain(domain)
|
||||||
return strings.Replace(match, domain, randomDomain, 1)
|
|
||||||
}
|
}
|
||||||
return match
|
return match
|
||||||
})
|
})
|
||||||
@@ -201,6 +212,8 @@ func isWellKnown(addr netip.Addr) bool {
|
|||||||
"2606:4700:4700::1111", "2606:4700:4700::1001", // Cloudflare DNS IPv6
|
"2606:4700:4700::1111", "2606:4700:4700::1001", // Cloudflare DNS IPv6
|
||||||
"9.9.9.9", "149.112.112.112", // Quad9 DNS IPv4
|
"9.9.9.9", "149.112.112.112", // Quad9 DNS IPv4
|
||||||
"2620:fe::fe", "2620:fe::9", // Quad9 DNS IPv6
|
"2620:fe::fe", "2620:fe::9", // Quad9 DNS IPv6
|
||||||
|
|
||||||
|
"128.0.0.0", "8000::", // 2nd split subnet for default routes
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(wellKnown, addr.String()) {
|
if slices.Contains(wellKnown, addr.String()) {
|
||||||
|
|||||||
@@ -46,11 +46,59 @@ func TestAnonymizeIP(t *testing.T) {
|
|||||||
|
|
||||||
func TestAnonymizeDNSLogLine(t *testing.T) {
|
func TestAnonymizeDNSLogLine(t *testing.T) {
|
||||||
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||||
testLog := `2024-04-23T20:01:11+02:00 TRAC client/internal/dns/local.go:25: received question: dns.Question{Name:"example.com", Qtype:0x1c, Qclass:0x1}`
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
original string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Basic domain with trailing content",
|
||||||
|
input: "received DNS request for DNS forwarder: domain=example.com: something happened with code=123",
|
||||||
|
original: "example.com",
|
||||||
|
expect: `received DNS request for DNS forwarder: domain=anon-[a-zA-Z0-9]+\.domain: something happened with code=123`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Domain with trailing dot",
|
||||||
|
input: "domain=example.com. processing request with status=pending",
|
||||||
|
original: "example.com",
|
||||||
|
expect: `domain=anon-[a-zA-Z0-9]+\.domain\. processing request with status=pending`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple domains in log",
|
||||||
|
input: "forward domain=first.com status=ok, redirect to domain=second.com port=443",
|
||||||
|
original: "first.com", // testing just one is sufficient as AnonymizeDomain is tested separately
|
||||||
|
expect: `forward domain=anon-[a-zA-Z0-9]+\.domain status=ok, redirect to domain=anon-[a-zA-Z0-9]+\.domain port=443`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Already anonymized domain",
|
||||||
|
input: "got request domain=anon-xyz123.domain from=client1 to=server2",
|
||||||
|
original: "", // nothing should be anonymized
|
||||||
|
expect: `got request domain=anon-xyz123\.domain from=client1 to=server2`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Subdomain with trailing dot",
|
||||||
|
input: "domain=sub.example.com. next_hop=10.0.0.1 proto=udp",
|
||||||
|
original: "example.com",
|
||||||
|
expect: `domain=sub\.anon-[a-zA-Z0-9]+\.domain\. next_hop=10\.0\.0\.1 proto=udp`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Handler chain pattern log",
|
||||||
|
input: "pattern: domain=example.com. original: domain=*.example.com. wildcard=true priority=100",
|
||||||
|
original: "example.com",
|
||||||
|
expect: `pattern: domain=anon-[a-zA-Z0-9]+\.domain\. original: domain=\*\.anon-[a-zA-Z0-9]+\.domain\. wildcard=true priority=100`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
result := anonymizer.AnonymizeDNSLogLine(testLog)
|
for _, tc := range tests {
|
||||||
require.NotEqual(t, testLog, result)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
assert.NotContains(t, result, "example.com")
|
result := anonymizer.AnonymizeDNSLogLine(tc.input)
|
||||||
|
if tc.original != "" {
|
||||||
|
assert.NotContains(t, result, tc.original)
|
||||||
|
}
|
||||||
|
assert.Regexp(t, tc.expect, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnonymizeDomain(t *testing.T) {
|
func TestAnonymizeDomain(t *testing.T) {
|
||||||
@@ -67,18 +115,36 @@ func TestAnonymizeDomain(t *testing.T) {
|
|||||||
`^anon-[a-zA-Z0-9]+\.domain$`,
|
`^anon-[a-zA-Z0-9]+\.domain$`,
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Domain with Trailing Dot",
|
||||||
|
"example.com.",
|
||||||
|
`^anon-[a-zA-Z0-9]+\.domain.$`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Subdomain",
|
"Subdomain",
|
||||||
"sub.example.com",
|
"sub.example.com",
|
||||||
`^sub\.anon-[a-zA-Z0-9]+\.domain$`,
|
`^sub\.anon-[a-zA-Z0-9]+\.domain$`,
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Subdomain with Trailing Dot",
|
||||||
|
"sub.example.com.",
|
||||||
|
`^sub\.anon-[a-zA-Z0-9]+\.domain.$`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Protected Domain",
|
"Protected Domain",
|
||||||
"netbird.io",
|
"netbird.io",
|
||||||
`^netbird\.io$`,
|
`^netbird\.io$`,
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Protected Domain with Trailing Dot",
|
||||||
|
"netbird.io.",
|
||||||
|
`^netbird\.io.$`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
@@ -140,8 +206,16 @@ func TestAnonymizeSchemeURI(t *testing.T) {
|
|||||||
expect string
|
expect string
|
||||||
}{
|
}{
|
||||||
{"STUN URI in text", "Connection made via stun:example.com", `Connection made via stun:anon-[a-zA-Z0-9]+\.domain`},
|
{"STUN URI in text", "Connection made via stun:example.com", `Connection made via stun:anon-[a-zA-Z0-9]+\.domain`},
|
||||||
|
{"STUNS URI in message", "Secure connection to stuns:example.com:443", `Secure connection to stuns:anon-[a-zA-Z0-9]+\.domain:443`},
|
||||||
{"TURN URI in log", "Failed attempt turn:some.example.com:3478?transport=tcp: retrying", `Failed attempt turn:some.anon-[a-zA-Z0-9]+\.domain:3478\?transport=tcp: retrying`},
|
{"TURN URI in log", "Failed attempt turn:some.example.com:3478?transport=tcp: retrying", `Failed attempt turn:some.anon-[a-zA-Z0-9]+\.domain:3478\?transport=tcp: retrying`},
|
||||||
|
{"TURNS URI in message", "Secure connection to turns:example.com:5349", `Secure connection to turns:anon-[a-zA-Z0-9]+\.domain:5349`},
|
||||||
|
{"HTTP URI in text", "Visit http://example.com for more", `Visit http://anon-[a-zA-Z0-9]+\.domain for more`},
|
||||||
|
{"HTTPS URI in CAPS", "Visit HTTPS://example.com for more", `Visit https://anon-[a-zA-Z0-9]+\.domain for more`},
|
||||||
{"HTTPS URI in message", "Visit https://example.com for more", `Visit https://anon-[a-zA-Z0-9]+\.domain for more`},
|
{"HTTPS URI in message", "Visit https://example.com for more", `Visit https://anon-[a-zA-Z0-9]+\.domain for more`},
|
||||||
|
{"WS URI in log", "Connection established to ws://example.com:8080", `Connection established to ws://anon-[a-zA-Z0-9]+\.domain:8080`},
|
||||||
|
{"WSS URI in message", "Secure connection to wss://example.com", `Secure connection to wss://anon-[a-zA-Z0-9]+\.domain`},
|
||||||
|
{"Rel URI in text", "Relaying to rel://example.com", `Relaying to rel://anon-[a-zA-Z0-9]+\.domain`},
|
||||||
|
{"Rels URI in message", "Relaying to rels://example.com", `Relaying to rels://anon-[a-zA-Z0-9]+\.domain`},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -61,6 +62,15 @@ var forCmd = &cobra.Command{
|
|||||||
RunE: runForDuration,
|
RunE: runForDuration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var persistenceCmd = &cobra.Command{
|
||||||
|
Use: "persistence [on|off]",
|
||||||
|
Short: "Set network map memory persistence",
|
||||||
|
Long: `Configure whether the latest network map should persist in memory. When enabled, the last known network map will be kept in memory.`,
|
||||||
|
Example: " netbird debug persistence on",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: setNetworkMapPersistence,
|
||||||
|
}
|
||||||
|
|
||||||
func debugBundle(cmd *cobra.Command, _ []string) error {
|
func debugBundle(cmd *cobra.Command, _ []string) error {
|
||||||
conn, err := getClient(cmd)
|
conn, err := getClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -171,6 +181,13 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
// Enable network map persistence before bringing the service up
|
||||||
|
if _, err := client.SetNetworkMapPersistence(cmd.Context(), &proto.SetNetworkMapPersistenceRequest{
|
||||||
|
Enabled: true,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable network map persistence: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
|
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
|
||||||
return fmt.Errorf("failed to up: %v", status.Convert(err).Message())
|
return fmt.Errorf("failed to up: %v", status.Convert(err).Message())
|
||||||
}
|
}
|
||||||
@@ -200,6 +217,13 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
|
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable network map persistence after creating the debug bundle
|
||||||
|
if _, err := client.SetNetworkMapPersistence(cmd.Context(), &proto.SetNetworkMapPersistenceRequest{
|
||||||
|
Enabled: false,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("failed to disable network map persistence: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
if stateWasDown {
|
if stateWasDown {
|
||||||
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
||||||
return fmt.Errorf("failed to down: %v", status.Convert(err).Message())
|
return fmt.Errorf("failed to down: %v", status.Convert(err).Message())
|
||||||
@@ -219,6 +243,34 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setNetworkMapPersistence(cmd *cobra.Command, args []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Errorf(errCloseConnection, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
persistence := strings.ToLower(args[0])
|
||||||
|
if persistence != "on" && persistence != "off" {
|
||||||
|
return fmt.Errorf("invalid persistence value: %s. Use 'on' or 'off'", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
_, err = client.SetNetworkMapPersistence(cmd.Context(), &proto.SetNetworkMapPersistenceRequest{
|
||||||
|
Enabled: persistence == "on",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set network map persistence: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Printf("Network map persistence set to: %s\n", persistence)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getStatusOutput(cmd *cobra.Command) string {
|
func getStatusOutput(cmd *cobra.Command) string {
|
||||||
var statusOutputString string
|
var statusOutputString string
|
||||||
statusResp, err := getStatus(cmd.Context())
|
statusResp, err := getStatus(cmd.Context())
|
||||||
|
|||||||
173
client/cmd/networks.go
Normal file
173
client/cmd/networks.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var appendFlag bool
|
||||||
|
|
||||||
|
var networksCMD = &cobra.Command{
|
||||||
|
Use: "networks",
|
||||||
|
Aliases: []string{"routes"},
|
||||||
|
Short: "Manage networks",
|
||||||
|
Long: `Commands to list, select, or deselect networks. Replaces the "routes" command.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var routesListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Short: "List networks",
|
||||||
|
Example: " netbird networks list",
|
||||||
|
Long: "List all available network routes.",
|
||||||
|
RunE: networksList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var routesSelectCmd = &cobra.Command{
|
||||||
|
Use: "select network...|all",
|
||||||
|
Short: "Select network",
|
||||||
|
Long: "Select a list of networks by identifiers or 'all' to clear all selections and to accept all (including new) networks.\nDefault mode is replace, use -a to append to already selected networks.",
|
||||||
|
Example: " netbird networks select all\n netbird networks select route1 route2\n netbird routes select -a route3",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: networksSelect,
|
||||||
|
}
|
||||||
|
|
||||||
|
var routesDeselectCmd = &cobra.Command{
|
||||||
|
Use: "deselect network...|all",
|
||||||
|
Short: "Deselect networks",
|
||||||
|
Long: "Deselect previously selected networks by identifiers or 'all' to disable accepting any networks.",
|
||||||
|
Example: " netbird networks deselect all\n netbird networks deselect route1 route2",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: networksDeselect,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
routesSelectCmd.PersistentFlags().BoolVarP(&appendFlag, "append", "a", false, "Append to current network selection instead of replacing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func networksList(cmd *cobra.Command, _ []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
resp, err := client.ListNetworks(cmd.Context(), &proto.ListNetworksRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list network: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Routes) == 0 {
|
||||||
|
cmd.Println("No networks available.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
printNetworks(cmd, resp)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNetworks(cmd *cobra.Command, resp *proto.ListNetworksResponse) {
|
||||||
|
cmd.Println("Available Networks:")
|
||||||
|
for _, route := range resp.Routes {
|
||||||
|
printNetwork(cmd, route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNetwork(cmd *cobra.Command, route *proto.Network) {
|
||||||
|
selectedStatus := getSelectedStatus(route)
|
||||||
|
domains := route.GetDomains()
|
||||||
|
|
||||||
|
if len(domains) > 0 {
|
||||||
|
printDomainRoute(cmd, route, domains, selectedStatus)
|
||||||
|
} else {
|
||||||
|
printNetworkRoute(cmd, route, selectedStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSelectedStatus(route *proto.Network) string {
|
||||||
|
if route.GetSelected() {
|
||||||
|
return "Selected"
|
||||||
|
}
|
||||||
|
return "Not Selected"
|
||||||
|
}
|
||||||
|
|
||||||
|
func printDomainRoute(cmd *cobra.Command, route *proto.Network, domains []string, selectedStatus string) {
|
||||||
|
cmd.Printf("\n - ID: %s\n Domains: %s\n Status: %s\n", route.GetID(), strings.Join(domains, ", "), selectedStatus)
|
||||||
|
resolvedIPs := route.GetResolvedIPs()
|
||||||
|
|
||||||
|
if len(resolvedIPs) > 0 {
|
||||||
|
printResolvedIPs(cmd, domains, resolvedIPs)
|
||||||
|
} else {
|
||||||
|
cmd.Printf(" Resolved IPs: -\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNetworkRoute(cmd *cobra.Command, route *proto.Network, selectedStatus string) {
|
||||||
|
cmd.Printf("\n - ID: %s\n Network: %s\n Status: %s\n", route.GetID(), route.GetRange(), selectedStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printResolvedIPs(cmd *cobra.Command, _ []string, resolvedIPs map[string]*proto.IPList) {
|
||||||
|
cmd.Printf(" Resolved IPs:\n")
|
||||||
|
for resolvedDomain, ipList := range resolvedIPs {
|
||||||
|
cmd.Printf(" [%s]: %s\n", resolvedDomain, strings.Join(ipList.GetIps(), ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func networksSelect(cmd *cobra.Command, args []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
req := &proto.SelectNetworksRequest{
|
||||||
|
NetworkIDs: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 1 && args[0] == "all" {
|
||||||
|
req.All = true
|
||||||
|
} else if appendFlag {
|
||||||
|
req.Append = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.SelectNetworks(cmd.Context(), req); err != nil {
|
||||||
|
return fmt.Errorf("failed to select networks: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Networks selected successfully.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func networksDeselect(cmd *cobra.Command, args []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
req := &proto.SelectNetworksRequest{
|
||||||
|
NetworkIDs: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 1 && args[0] == "all" {
|
||||||
|
req.All = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.DeselectNetworks(cmd.Context(), req); err != nil {
|
||||||
|
return fmt.Errorf("failed to deselect networks: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Networks deselected successfully.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
33
client/cmd/pprof.go
Normal file
33
client/cmd/pprof.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
//go:build pprof
|
||||||
|
// +build pprof
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
_ "net/http/pprof"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
addr := pprofAddr()
|
||||||
|
go pprof(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pprofAddr() string {
|
||||||
|
listenAddr := os.Getenv("NB_PPROF_ADDR")
|
||||||
|
if listenAddr == "" {
|
||||||
|
return "localhost:6969"
|
||||||
|
}
|
||||||
|
|
||||||
|
return listenAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func pprof(listenAddr string) {
|
||||||
|
log.Infof("listening pprof on: %s\n", listenAddr)
|
||||||
|
if err := http.ListenAndServe(listenAddr, nil); err != nil {
|
||||||
|
log.Fatalf("Failed to start pprof: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -142,19 +142,20 @@ func init() {
|
|||||||
rootCmd.AddCommand(loginCmd)
|
rootCmd.AddCommand(loginCmd)
|
||||||
rootCmd.AddCommand(versionCmd)
|
rootCmd.AddCommand(versionCmd)
|
||||||
rootCmd.AddCommand(sshCmd)
|
rootCmd.AddCommand(sshCmd)
|
||||||
rootCmd.AddCommand(routesCmd)
|
rootCmd.AddCommand(networksCMD)
|
||||||
rootCmd.AddCommand(debugCmd)
|
rootCmd.AddCommand(debugCmd)
|
||||||
|
|
||||||
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
|
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
|
||||||
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
|
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
|
||||||
|
|
||||||
routesCmd.AddCommand(routesListCmd)
|
networksCMD.AddCommand(routesListCmd)
|
||||||
routesCmd.AddCommand(routesSelectCmd, routesDeselectCmd)
|
networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd)
|
||||||
|
|
||||||
debugCmd.AddCommand(debugBundleCmd)
|
debugCmd.AddCommand(debugBundleCmd)
|
||||||
debugCmd.AddCommand(logCmd)
|
debugCmd.AddCommand(logCmd)
|
||||||
logCmd.AddCommand(logLevelCmd)
|
logCmd.AddCommand(logLevelCmd)
|
||||||
debugCmd.AddCommand(forCmd)
|
debugCmd.AddCommand(forCmd)
|
||||||
|
debugCmd.AddCommand(persistenceCmd)
|
||||||
|
|
||||||
upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil,
|
upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil,
|
||||||
`Sets external IPs maps between local addresses and interfaces.`+
|
`Sets external IPs maps between local addresses and interfaces.`+
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
var appendFlag bool
|
|
||||||
|
|
||||||
var routesCmd = &cobra.Command{
|
|
||||||
Use: "routes",
|
|
||||||
Short: "Manage network routes",
|
|
||||||
Long: `Commands to list, select, or deselect network routes.`,
|
|
||||||
}
|
|
||||||
|
|
||||||
var routesListCmd = &cobra.Command{
|
|
||||||
Use: "list",
|
|
||||||
Aliases: []string{"ls"},
|
|
||||||
Short: "List routes",
|
|
||||||
Example: " netbird routes list",
|
|
||||||
Long: "List all available network routes.",
|
|
||||||
RunE: routesList,
|
|
||||||
}
|
|
||||||
|
|
||||||
var routesSelectCmd = &cobra.Command{
|
|
||||||
Use: "select route...|all",
|
|
||||||
Short: "Select routes",
|
|
||||||
Long: "Select a list of routes by identifiers or 'all' to clear all selections and to accept all (including new) routes.\nDefault mode is replace, use -a to append to already selected routes.",
|
|
||||||
Example: " netbird routes select all\n netbird routes select route1 route2\n netbird routes select -a route3",
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
RunE: routesSelect,
|
|
||||||
}
|
|
||||||
|
|
||||||
var routesDeselectCmd = &cobra.Command{
|
|
||||||
Use: "deselect route...|all",
|
|
||||||
Short: "Deselect routes",
|
|
||||||
Long: "Deselect previously selected routes by identifiers or 'all' to disable accepting any routes.",
|
|
||||||
Example: " netbird routes deselect all\n netbird routes deselect route1 route2",
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
RunE: routesDeselect,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
routesSelectCmd.PersistentFlags().BoolVarP(&appendFlag, "append", "a", false, "Append to current route selection instead of replacing")
|
|
||||||
}
|
|
||||||
|
|
||||||
func routesList(cmd *cobra.Command, _ []string) error {
|
|
||||||
conn, err := getClient(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
client := proto.NewDaemonServiceClient(conn)
|
|
||||||
resp, err := client.ListRoutes(cmd.Context(), &proto.ListRoutesRequest{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to list routes: %v", status.Convert(err).Message())
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resp.Routes) == 0 {
|
|
||||||
cmd.Println("No routes available.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
printRoutes(cmd, resp)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printRoutes(cmd *cobra.Command, resp *proto.ListRoutesResponse) {
|
|
||||||
cmd.Println("Available Routes:")
|
|
||||||
for _, route := range resp.Routes {
|
|
||||||
printRoute(cmd, route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printRoute(cmd *cobra.Command, route *proto.Route) {
|
|
||||||
selectedStatus := getSelectedStatus(route)
|
|
||||||
domains := route.GetDomains()
|
|
||||||
|
|
||||||
if len(domains) > 0 {
|
|
||||||
printDomainRoute(cmd, route, domains, selectedStatus)
|
|
||||||
} else {
|
|
||||||
printNetworkRoute(cmd, route, selectedStatus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSelectedStatus(route *proto.Route) string {
|
|
||||||
if route.GetSelected() {
|
|
||||||
return "Selected"
|
|
||||||
}
|
|
||||||
return "Not Selected"
|
|
||||||
}
|
|
||||||
|
|
||||||
func printDomainRoute(cmd *cobra.Command, route *proto.Route, domains []string, selectedStatus string) {
|
|
||||||
cmd.Printf("\n - ID: %s\n Domains: %s\n Status: %s\n", route.GetID(), strings.Join(domains, ", "), selectedStatus)
|
|
||||||
resolvedIPs := route.GetResolvedIPs()
|
|
||||||
|
|
||||||
if len(resolvedIPs) > 0 {
|
|
||||||
printResolvedIPs(cmd, domains, resolvedIPs)
|
|
||||||
} else {
|
|
||||||
cmd.Printf(" Resolved IPs: -\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printNetworkRoute(cmd *cobra.Command, route *proto.Route, selectedStatus string) {
|
|
||||||
cmd.Printf("\n - ID: %s\n Network: %s\n Status: %s\n", route.GetID(), route.GetNetwork(), selectedStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
func printResolvedIPs(cmd *cobra.Command, domains []string, resolvedIPs map[string]*proto.IPList) {
|
|
||||||
cmd.Printf(" Resolved IPs:\n")
|
|
||||||
for _, domain := range domains {
|
|
||||||
if ipList, exists := resolvedIPs[domain]; exists {
|
|
||||||
cmd.Printf(" [%s]: %s\n", domain, strings.Join(ipList.GetIps(), ", "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func routesSelect(cmd *cobra.Command, args []string) error {
|
|
||||||
conn, err := getClient(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
client := proto.NewDaemonServiceClient(conn)
|
|
||||||
req := &proto.SelectRoutesRequest{
|
|
||||||
RouteIDs: args,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) == 1 && args[0] == "all" {
|
|
||||||
req.All = true
|
|
||||||
} else if appendFlag {
|
|
||||||
req.Append = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := client.SelectRoutes(cmd.Context(), req); err != nil {
|
|
||||||
return fmt.Errorf("failed to select routes: %v", status.Convert(err).Message())
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("Routes selected successfully.")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func routesDeselect(cmd *cobra.Command, args []string) error {
|
|
||||||
conn, err := getClient(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
client := proto.NewDaemonServiceClient(conn)
|
|
||||||
req := &proto.SelectRoutesRequest{
|
|
||||||
RouteIDs: args,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) == 1 && args[0] == "all" {
|
|
||||||
req.All = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := client.DeselectRoutes(cmd.Context(), req); err != nil {
|
|
||||||
return fmt.Errorf("failed to deselect routes: %v", status.Convert(err).Message())
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("Routes deselected successfully.")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -13,10 +14,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type program struct {
|
type program struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
serv *grpc.Server
|
serv *grpc.Server
|
||||||
serverInstance *server.Server
|
serverInstance *server.Server
|
||||||
|
serverInstanceMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
|
func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ func (p *program) Start(svc service.Service) error {
|
|||||||
}
|
}
|
||||||
proto.RegisterDaemonServiceServer(p.serv, serverInstance)
|
proto.RegisterDaemonServiceServer(p.serv, serverInstance)
|
||||||
|
|
||||||
|
p.serverInstanceMu.Lock()
|
||||||
p.serverInstance = serverInstance
|
p.serverInstance = serverInstance
|
||||||
|
p.serverInstanceMu.Unlock()
|
||||||
|
|
||||||
log.Printf("started daemon server: %v", split[1])
|
log.Printf("started daemon server: %v", split[1])
|
||||||
if err := p.serv.Serve(listen); err != nil {
|
if err := p.serv.Serve(listen); err != nil {
|
||||||
@@ -72,6 +74,7 @@ func (p *program) Start(svc service.Service) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *program) Stop(srv service.Service) error {
|
func (p *program) Stop(srv service.Service) error {
|
||||||
|
p.serverInstanceMu.Lock()
|
||||||
if p.serverInstance != nil {
|
if p.serverInstance != nil {
|
||||||
in := new(proto.DownRequest)
|
in := new(proto.DownRequest)
|
||||||
_, err := p.serverInstance.Down(p.ctx, in)
|
_, err := p.serverInstance.Down(p.ctx, in)
|
||||||
@@ -79,6 +82,7 @@ func (p *program) Stop(srv service.Service) error {
|
|||||||
log.Errorf("failed to stop daemon: %v", err)
|
log.Errorf("failed to stop daemon: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
p.serverInstanceMu.Unlock()
|
||||||
|
|
||||||
p.cancel()
|
p.cancel()
|
||||||
|
|
||||||
|
|||||||
181
client/cmd/state.go
Normal file
181
client/cmd/state.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
allFlag bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var stateCmd = &cobra.Command{
|
||||||
|
Use: "state",
|
||||||
|
Short: "Manage daemon state",
|
||||||
|
Long: "Provides commands for managing and inspecting the Netbird daemon state.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var stateListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Short: "List all stored states",
|
||||||
|
Long: "Lists all registered states with their status and basic information.",
|
||||||
|
Example: " netbird state list",
|
||||||
|
RunE: stateList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var stateCleanCmd = &cobra.Command{
|
||||||
|
Use: "clean [state-name]",
|
||||||
|
Short: "Clean stored states",
|
||||||
|
Long: `Clean specific state or all states. The daemon must not be running.
|
||||||
|
This will perform cleanup operations and remove the state.`,
|
||||||
|
Example: ` netbird state clean dns_state
|
||||||
|
netbird state clean --all`,
|
||||||
|
RunE: stateClean,
|
||||||
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// Check mutual exclusivity between --all flag and state-name argument
|
||||||
|
if allFlag && len(args) > 0 {
|
||||||
|
return fmt.Errorf("cannot specify both --all flag and state name")
|
||||||
|
}
|
||||||
|
if !allFlag && len(args) != 1 {
|
||||||
|
return fmt.Errorf("requires a state name argument or --all flag")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var stateDeleteCmd = &cobra.Command{
|
||||||
|
Use: "delete [state-name]",
|
||||||
|
Short: "Delete stored states",
|
||||||
|
Long: `Delete specific state or all states from storage. The daemon must not be running.
|
||||||
|
This will remove the state without performing any cleanup operations.`,
|
||||||
|
Example: ` netbird state delete dns_state
|
||||||
|
netbird state delete --all`,
|
||||||
|
RunE: stateDelete,
|
||||||
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// Check mutual exclusivity between --all flag and state-name argument
|
||||||
|
if allFlag && len(args) > 0 {
|
||||||
|
return fmt.Errorf("cannot specify both --all flag and state name")
|
||||||
|
}
|
||||||
|
if !allFlag && len(args) != 1 {
|
||||||
|
return fmt.Errorf("requires a state name argument or --all flag")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(stateCmd)
|
||||||
|
stateCmd.AddCommand(stateListCmd, stateCleanCmd, stateDeleteCmd)
|
||||||
|
|
||||||
|
stateCleanCmd.Flags().BoolVarP(&allFlag, "all", "a", false, "Clean all states")
|
||||||
|
stateDeleteCmd.Flags().BoolVarP(&allFlag, "all", "a", false, "Delete all states")
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateList(cmd *cobra.Command, _ []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Errorf(errCloseConnection, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
resp, err := client.ListStates(cmd.Context(), &proto.ListStatesRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list states: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Printf("\nStored states:\n\n")
|
||||||
|
for _, state := range resp.States {
|
||||||
|
cmd.Printf("- %s\n", state.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateClean(cmd *cobra.Command, args []string) error {
|
||||||
|
var stateName string
|
||||||
|
if !allFlag {
|
||||||
|
stateName = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Errorf(errCloseConnection, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
resp, err := client.CleanState(cmd.Context(), &proto.CleanStateRequest{
|
||||||
|
StateName: stateName,
|
||||||
|
All: allFlag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to clean state: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.CleanedStates == 0 {
|
||||||
|
cmd.Println("No states were cleaned")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if allFlag {
|
||||||
|
cmd.Printf("Successfully cleaned %d states\n", resp.CleanedStates)
|
||||||
|
} else {
|
||||||
|
cmd.Printf("Successfully cleaned state %q\n", stateName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateDelete(cmd *cobra.Command, args []string) error {
|
||||||
|
var stateName string
|
||||||
|
if !allFlag {
|
||||||
|
stateName = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Errorf(errCloseConnection, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
resp, err := client.DeleteState(cmd.Context(), &proto.DeleteStateRequest{
|
||||||
|
StateName: stateName,
|
||||||
|
All: allFlag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete state: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.DeletedStates == 0 {
|
||||||
|
cmd.Println("No states were deleted")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if allFlag {
|
||||||
|
cmd.Printf("Successfully deleted %d states\n", resp.DeletedStates)
|
||||||
|
} else {
|
||||||
|
cmd.Printf("Successfully deleted state %q\n", stateName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ type peerStateDetailOutput struct {
|
|||||||
Latency time.Duration `json:"latency" yaml:"latency"`
|
Latency time.Duration `json:"latency" yaml:"latency"`
|
||||||
RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"`
|
RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"`
|
||||||
Routes []string `json:"routes" yaml:"routes"`
|
Routes []string `json:"routes" yaml:"routes"`
|
||||||
|
Networks []string `json:"networks" yaml:"networks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type peersStateOutput struct {
|
type peersStateOutput struct {
|
||||||
@@ -98,6 +99,7 @@ type statusOutputOverview struct {
|
|||||||
RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"`
|
RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"`
|
||||||
RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"`
|
RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"`
|
||||||
Routes []string `json:"routes" yaml:"routes"`
|
Routes []string `json:"routes" yaml:"routes"`
|
||||||
|
Networks []string `json:"networks" yaml:"networks"`
|
||||||
NSServerGroups []nsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"`
|
NSServerGroups []nsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +284,8 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv
|
|||||||
FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(),
|
FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(),
|
||||||
RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(),
|
RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(),
|
||||||
RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(),
|
RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(),
|
||||||
Routes: pbFullStatus.GetLocalPeerState().GetRoutes(),
|
Routes: pbFullStatus.GetLocalPeerState().GetNetworks(),
|
||||||
|
Networks: pbFullStatus.GetLocalPeerState().GetNetworks(),
|
||||||
NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()),
|
NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +393,8 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput {
|
|||||||
TransferSent: transferSent,
|
TransferSent: transferSent,
|
||||||
Latency: pbPeerState.GetLatency().AsDuration(),
|
Latency: pbPeerState.GetLatency().AsDuration(),
|
||||||
RosenpassEnabled: pbPeerState.GetRosenpassEnabled(),
|
RosenpassEnabled: pbPeerState.GetRosenpassEnabled(),
|
||||||
Routes: pbPeerState.GetRoutes(),
|
Routes: pbPeerState.GetNetworks(),
|
||||||
|
Networks: pbPeerState.GetNetworks(),
|
||||||
}
|
}
|
||||||
|
|
||||||
peersStateDetail = append(peersStateDetail, peerState)
|
peersStateDetail = append(peersStateDetail, peerState)
|
||||||
@@ -491,10 +495,10 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
|
|||||||
relaysString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total)
|
relaysString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total)
|
||||||
}
|
}
|
||||||
|
|
||||||
routes := "-"
|
networks := "-"
|
||||||
if len(overview.Routes) > 0 {
|
if len(overview.Networks) > 0 {
|
||||||
sort.Strings(overview.Routes)
|
sort.Strings(overview.Networks)
|
||||||
routes = strings.Join(overview.Routes, ", ")
|
networks = strings.Join(overview.Networks, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
var dnsServersString string
|
var dnsServersString string
|
||||||
@@ -556,6 +560,7 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
|
|||||||
"Interface type: %s\n"+
|
"Interface type: %s\n"+
|
||||||
"Quantum resistance: %s\n"+
|
"Quantum resistance: %s\n"+
|
||||||
"Routes: %s\n"+
|
"Routes: %s\n"+
|
||||||
|
"Networks: %s\n"+
|
||||||
"Peers count: %s\n",
|
"Peers count: %s\n",
|
||||||
fmt.Sprintf("%s/%s%s", goos, goarch, goarm),
|
fmt.Sprintf("%s/%s%s", goos, goarch, goarm),
|
||||||
overview.DaemonVersion,
|
overview.DaemonVersion,
|
||||||
@@ -568,7 +573,8 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
|
|||||||
interfaceIP,
|
interfaceIP,
|
||||||
interfaceTypeString,
|
interfaceTypeString,
|
||||||
rosenpassEnabledStatus,
|
rosenpassEnabledStatus,
|
||||||
routes,
|
networks,
|
||||||
|
networks,
|
||||||
peersCountString,
|
peersCountString,
|
||||||
)
|
)
|
||||||
return summary
|
return summary
|
||||||
@@ -631,10 +637,10 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
routes := "-"
|
networks := "-"
|
||||||
if len(peerState.Routes) > 0 {
|
if len(peerState.Networks) > 0 {
|
||||||
sort.Strings(peerState.Routes)
|
sort.Strings(peerState.Networks)
|
||||||
routes = strings.Join(peerState.Routes, ", ")
|
networks = strings.Join(peerState.Networks, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
peerString := fmt.Sprintf(
|
peerString := fmt.Sprintf(
|
||||||
@@ -652,6 +658,7 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
|
|||||||
" Transfer status (received/sent) %s/%s\n"+
|
" Transfer status (received/sent) %s/%s\n"+
|
||||||
" Quantum resistance: %s\n"+
|
" Quantum resistance: %s\n"+
|
||||||
" Routes: %s\n"+
|
" Routes: %s\n"+
|
||||||
|
" Networks: %s\n"+
|
||||||
" Latency: %s\n",
|
" Latency: %s\n",
|
||||||
peerState.FQDN,
|
peerState.FQDN,
|
||||||
peerState.IP,
|
peerState.IP,
|
||||||
@@ -668,7 +675,8 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
|
|||||||
toIEC(peerState.TransferReceived),
|
toIEC(peerState.TransferReceived),
|
||||||
toIEC(peerState.TransferSent),
|
toIEC(peerState.TransferSent),
|
||||||
rosenpassEnabledStatus,
|
rosenpassEnabledStatus,
|
||||||
routes,
|
networks,
|
||||||
|
networks,
|
||||||
peerState.Latency.String(),
|
peerState.Latency.String(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -680,7 +688,7 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
|
|||||||
func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool {
|
func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool {
|
||||||
statusEval := false
|
statusEval := false
|
||||||
ipEval := false
|
ipEval := false
|
||||||
nameEval := false
|
nameEval := true
|
||||||
|
|
||||||
if statusFilter != "" {
|
if statusFilter != "" {
|
||||||
lowerStatusFilter := strings.ToLower(statusFilter)
|
lowerStatusFilter := strings.ToLower(statusFilter)
|
||||||
@@ -700,11 +708,13 @@ func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool {
|
|||||||
|
|
||||||
if len(prefixNamesFilter) > 0 {
|
if len(prefixNamesFilter) > 0 {
|
||||||
for prefixNameFilter := range prefixNamesFilterMap {
|
for prefixNameFilter := range prefixNamesFilterMap {
|
||||||
if !strings.HasPrefix(peerState.Fqdn, prefixNameFilter) {
|
if strings.HasPrefix(peerState.Fqdn, prefixNameFilter) {
|
||||||
nameEval = true
|
nameEval = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
nameEval = false
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusEval || ipEval || nameEval
|
return statusEval || ipEval || nameEval
|
||||||
@@ -808,6 +818,14 @@ func anonymizePeerDetail(a *anonymize.Anonymizer, peer *peerStateDetailOutput) {
|
|||||||
|
|
||||||
peer.RelayAddress = a.AnonymizeURI(peer.RelayAddress)
|
peer.RelayAddress = a.AnonymizeURI(peer.RelayAddress)
|
||||||
|
|
||||||
|
for i, route := range peer.Networks {
|
||||||
|
peer.Networks[i] = a.AnonymizeIPString(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, route := range peer.Networks {
|
||||||
|
peer.Networks[i] = a.AnonymizeRoute(route)
|
||||||
|
}
|
||||||
|
|
||||||
for i, route := range peer.Routes {
|
for i, route := range peer.Routes {
|
||||||
peer.Routes[i] = a.AnonymizeIPString(route)
|
peer.Routes[i] = a.AnonymizeIPString(route)
|
||||||
}
|
}
|
||||||
@@ -848,6 +866,10 @@ func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i, route := range overview.Networks {
|
||||||
|
overview.Networks[i] = a.AnonymizeRoute(route)
|
||||||
|
}
|
||||||
|
|
||||||
for i, route := range overview.Routes {
|
for i, route := range overview.Routes {
|
||||||
overview.Routes[i] = a.AnonymizeRoute(route)
|
overview.Routes[i] = a.AnonymizeRoute(route)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ var resp = &proto.StatusResponse{
|
|||||||
LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)),
|
LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)),
|
||||||
BytesRx: 200,
|
BytesRx: 200,
|
||||||
BytesTx: 100,
|
BytesTx: 100,
|
||||||
Routes: []string{
|
Networks: []string{
|
||||||
"10.1.0.0/24",
|
"10.1.0.0/24",
|
||||||
},
|
},
|
||||||
Latency: durationpb.New(time.Duration(10000000)),
|
Latency: durationpb.New(time.Duration(10000000)),
|
||||||
@@ -93,7 +93,7 @@ var resp = &proto.StatusResponse{
|
|||||||
PubKey: "Some-Pub-Key",
|
PubKey: "Some-Pub-Key",
|
||||||
KernelInterface: true,
|
KernelInterface: true,
|
||||||
Fqdn: "some-localhost.awesome-domain.com",
|
Fqdn: "some-localhost.awesome-domain.com",
|
||||||
Routes: []string{
|
Networks: []string{
|
||||||
"10.10.0.0/24",
|
"10.10.0.0/24",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -149,6 +149,9 @@ var overview = statusOutputOverview{
|
|||||||
Routes: []string{
|
Routes: []string{
|
||||||
"10.1.0.0/24",
|
"10.1.0.0/24",
|
||||||
},
|
},
|
||||||
|
Networks: []string{
|
||||||
|
"10.1.0.0/24",
|
||||||
|
},
|
||||||
Latency: time.Duration(10000000),
|
Latency: time.Duration(10000000),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -230,6 +233,9 @@ var overview = statusOutputOverview{
|
|||||||
Routes: []string{
|
Routes: []string{
|
||||||
"10.10.0.0/24",
|
"10.10.0.0/24",
|
||||||
},
|
},
|
||||||
|
Networks: []string{
|
||||||
|
"10.10.0.0/24",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConversionFromFullStatusToOutputOverview(t *testing.T) {
|
func TestConversionFromFullStatusToOutputOverview(t *testing.T) {
|
||||||
@@ -295,6 +301,9 @@ func TestParsingToJSON(t *testing.T) {
|
|||||||
"quantumResistance": false,
|
"quantumResistance": false,
|
||||||
"routes": [
|
"routes": [
|
||||||
"10.1.0.0/24"
|
"10.1.0.0/24"
|
||||||
|
],
|
||||||
|
"networks": [
|
||||||
|
"10.1.0.0/24"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -318,7 +327,8 @@ func TestParsingToJSON(t *testing.T) {
|
|||||||
"transferSent": 1000,
|
"transferSent": 1000,
|
||||||
"latency": 10000000,
|
"latency": 10000000,
|
||||||
"quantumResistance": false,
|
"quantumResistance": false,
|
||||||
"routes": null
|
"routes": null,
|
||||||
|
"networks": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -359,6 +369,9 @@ func TestParsingToJSON(t *testing.T) {
|
|||||||
"routes": [
|
"routes": [
|
||||||
"10.10.0.0/24"
|
"10.10.0.0/24"
|
||||||
],
|
],
|
||||||
|
"networks": [
|
||||||
|
"10.10.0.0/24"
|
||||||
|
],
|
||||||
"dnsServers": [
|
"dnsServers": [
|
||||||
{
|
{
|
||||||
"servers": [
|
"servers": [
|
||||||
@@ -418,6 +431,8 @@ func TestParsingToYAML(t *testing.T) {
|
|||||||
quantumResistance: false
|
quantumResistance: false
|
||||||
routes:
|
routes:
|
||||||
- 10.1.0.0/24
|
- 10.1.0.0/24
|
||||||
|
networks:
|
||||||
|
- 10.1.0.0/24
|
||||||
- fqdn: peer-2.awesome-domain.com
|
- fqdn: peer-2.awesome-domain.com
|
||||||
netbirdIp: 192.168.178.102
|
netbirdIp: 192.168.178.102
|
||||||
publicKey: Pubkey2
|
publicKey: Pubkey2
|
||||||
@@ -437,6 +452,7 @@ func TestParsingToYAML(t *testing.T) {
|
|||||||
latency: 10ms
|
latency: 10ms
|
||||||
quantumResistance: false
|
quantumResistance: false
|
||||||
routes: []
|
routes: []
|
||||||
|
networks: []
|
||||||
cliVersion: development
|
cliVersion: development
|
||||||
daemonVersion: 0.14.1
|
daemonVersion: 0.14.1
|
||||||
management:
|
management:
|
||||||
@@ -465,6 +481,8 @@ quantumResistance: false
|
|||||||
quantumResistancePermissive: false
|
quantumResistancePermissive: false
|
||||||
routes:
|
routes:
|
||||||
- 10.10.0.0/24
|
- 10.10.0.0/24
|
||||||
|
networks:
|
||||||
|
- 10.10.0.0/24
|
||||||
dnsServers:
|
dnsServers:
|
||||||
- servers:
|
- servers:
|
||||||
- 8.8.8.8:53
|
- 8.8.8.8:53
|
||||||
@@ -509,6 +527,7 @@ func TestParsingToDetail(t *testing.T) {
|
|||||||
Transfer status (received/sent) 200 B/100 B
|
Transfer status (received/sent) 200 B/100 B
|
||||||
Quantum resistance: false
|
Quantum resistance: false
|
||||||
Routes: 10.1.0.0/24
|
Routes: 10.1.0.0/24
|
||||||
|
Networks: 10.1.0.0/24
|
||||||
Latency: 10ms
|
Latency: 10ms
|
||||||
|
|
||||||
peer-2.awesome-domain.com:
|
peer-2.awesome-domain.com:
|
||||||
@@ -525,6 +544,7 @@ func TestParsingToDetail(t *testing.T) {
|
|||||||
Transfer status (received/sent) 2.0 KiB/1000 B
|
Transfer status (received/sent) 2.0 KiB/1000 B
|
||||||
Quantum resistance: false
|
Quantum resistance: false
|
||||||
Routes: -
|
Routes: -
|
||||||
|
Networks: -
|
||||||
Latency: 10ms
|
Latency: 10ms
|
||||||
|
|
||||||
OS: %s/%s
|
OS: %s/%s
|
||||||
@@ -543,6 +563,7 @@ NetBird IP: 192.168.178.100/16
|
|||||||
Interface type: Kernel
|
Interface type: Kernel
|
||||||
Quantum resistance: false
|
Quantum resistance: false
|
||||||
Routes: 10.10.0.0/24
|
Routes: 10.10.0.0/24
|
||||||
|
Networks: 10.10.0.0/24
|
||||||
Peers count: 2/2 Connected
|
Peers count: 2/2 Connected
|
||||||
`, lastConnectionUpdate1, lastHandshake1, lastConnectionUpdate2, lastHandshake2, runtime.GOOS, runtime.GOARCH, overview.CliVersion)
|
`, lastConnectionUpdate1, lastHandshake1, lastConnectionUpdate2, lastHandshake2, runtime.GOOS, runtime.GOARCH, overview.CliVersion)
|
||||||
|
|
||||||
@@ -564,6 +585,7 @@ NetBird IP: 192.168.178.100/16
|
|||||||
Interface type: Kernel
|
Interface type: Kernel
|
||||||
Quantum resistance: false
|
Quantum resistance: false
|
||||||
Routes: 10.10.0.0/24
|
Routes: 10.10.0.0/24
|
||||||
|
Networks: 10.10.0.0/24
|
||||||
Peers count: 2/2 Connected
|
Peers count: 2/2 Connected
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
@@ -71,7 +73,7 @@ func startManagement(t *testing.T, config *mgmt.Config, testFile string) (*grpc.
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
s := grpc.NewServer()
|
s := grpc.NewServer()
|
||||||
store, cleanUp, err := mgmt.NewTestStoreFromSQL(context.Background(), testFile, t.TempDir())
|
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), testFile, t.TempDir())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -93,7 +95,7 @@ func startManagement(t *testing.T, config *mgmt.Config, testFile string) (*grpc.
|
|||||||
}
|
}
|
||||||
|
|
||||||
secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
|
secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
|
||||||
mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, peersUpdateManager, secretsManager, nil, nil)
|
mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,14 +352,14 @@ func (m *aclManager) seedInitialEntries() {
|
|||||||
func (m *aclManager) seedInitialOptionalEntries() {
|
func (m *aclManager) seedInitialOptionalEntries() {
|
||||||
m.optionalEntries["FORWARD"] = []entry{
|
m.optionalEntries["FORWARD"] = []entry{
|
||||||
{
|
{
|
||||||
spec: []string{"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmark), "-j", chainNameInputRules},
|
spec: []string{"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected), "-j", chainNameInputRules},
|
||||||
position: 2,
|
position: 2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
m.optionalEntries["PREROUTING"] = []entry{
|
m.optionalEntries["PREROUTING"] = []entry{
|
||||||
{
|
{
|
||||||
spec: []string{"-t", "mangle", "-i", m.wgIface.Name(), "-m", "addrtype", "--dst-type", "LOCAL", "-j", "MARK", "--set-mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmark)},
|
spec: []string{"-t", "mangle", "-i", m.wgIface.Name(), "-m", "addrtype", "--dst-type", "LOCAL", "-j", "MARK", "--set-mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected)},
|
||||||
position: 1,
|
position: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,9 +83,11 @@ func (m *Manager) Init(stateManager *statemanager.Manager) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// persist early to ensure cleanup of chains
|
// persist early to ensure cleanup of chains
|
||||||
if err := stateManager.PersistState(context.Background()); err != nil {
|
go func() {
|
||||||
log.Errorf("failed to persist state: %v", err)
|
if err := stateManager.PersistState(context.Background()); err != nil {
|
||||||
}
|
log.Errorf("failed to persist state: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,22 +18,24 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/acl/id"
|
"github.com/netbirdio/netbird/client/internal/acl/id"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
)
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
|
|
||||||
const (
|
|
||||||
ipv4Nat = "netbird-rt-nat"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// constants needed to manage and create iptable rules
|
// constants needed to manage and create iptable rules
|
||||||
const (
|
const (
|
||||||
tableFilter = "filter"
|
tableFilter = "filter"
|
||||||
tableNat = "nat"
|
tableNat = "nat"
|
||||||
|
tableMangle = "mangle"
|
||||||
chainPOSTROUTING = "POSTROUTING"
|
chainPOSTROUTING = "POSTROUTING"
|
||||||
|
chainPREROUTING = "PREROUTING"
|
||||||
chainRTNAT = "NETBIRD-RT-NAT"
|
chainRTNAT = "NETBIRD-RT-NAT"
|
||||||
chainRTFWD = "NETBIRD-RT-FWD"
|
chainRTFWD = "NETBIRD-RT-FWD"
|
||||||
|
chainRTPRE = "NETBIRD-RT-PRE"
|
||||||
routingFinalForwardJump = "ACCEPT"
|
routingFinalForwardJump = "ACCEPT"
|
||||||
routingFinalNatJump = "MASQUERADE"
|
routingFinalNatJump = "MASQUERADE"
|
||||||
|
|
||||||
|
jumpPre = "jump-pre"
|
||||||
|
jumpNat = "jump-nat"
|
||||||
matchSet = "--match-set"
|
matchSet = "--match-set"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -323,24 +325,25 @@ func (r *router) Reset() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) cleanUpDefaultForwardRules() error {
|
func (r *router) cleanUpDefaultForwardRules() error {
|
||||||
err := r.cleanJumpRules()
|
if err := r.cleanJumpRules(); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("clean jump rules: %w", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("flushing routing related tables")
|
log.Debug("flushing routing related tables")
|
||||||
for _, chain := range []string{chainRTFWD, chainRTNAT} {
|
for _, chainInfo := range []struct {
|
||||||
table := r.getTableForChain(chain)
|
chain string
|
||||||
|
table string
|
||||||
ok, err := r.iptablesClient.ChainExists(table, chain)
|
}{
|
||||||
|
{chainRTFWD, tableFilter},
|
||||||
|
{chainRTNAT, tableNat},
|
||||||
|
{chainRTPRE, tableMangle},
|
||||||
|
} {
|
||||||
|
ok, err := r.iptablesClient.ChainExists(chainInfo.table, chainInfo.chain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed check chain %s, error: %v", chain, err)
|
return fmt.Errorf("check chain %s in table %s: %w", chainInfo.chain, chainInfo.table, err)
|
||||||
return err
|
|
||||||
} else if ok {
|
} else if ok {
|
||||||
err = r.iptablesClient.ClearAndDeleteChain(table, chain)
|
if err = r.iptablesClient.ClearAndDeleteChain(chainInfo.table, chainInfo.chain); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("clear and delete chain %s in table %s: %w", chainInfo.chain, chainInfo.table, err)
|
||||||
log.Errorf("failed cleaning chain %s, error: %v", chain, err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,9 +352,16 @@ func (r *router) cleanUpDefaultForwardRules() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) createContainers() error {
|
func (r *router) createContainers() error {
|
||||||
for _, chain := range []string{chainRTFWD, chainRTNAT} {
|
for _, chainInfo := range []struct {
|
||||||
if err := r.createAndSetupChain(chain); err != nil {
|
chain string
|
||||||
return fmt.Errorf("create chain %s: %w", chain, err)
|
table string
|
||||||
|
}{
|
||||||
|
{chainRTFWD, tableFilter},
|
||||||
|
{chainRTPRE, tableMangle},
|
||||||
|
{chainRTNAT, tableNat},
|
||||||
|
} {
|
||||||
|
if err := r.createAndSetupChain(chainInfo.chain); err != nil {
|
||||||
|
return fmt.Errorf("create chain %s in table %s: %w", chainInfo.chain, chainInfo.table, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,6 +369,10 @@ func (r *router) createContainers() error {
|
|||||||
return fmt.Errorf("insert established rule: %w", err)
|
return fmt.Errorf("insert established rule: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := r.addPostroutingRules(); err != nil {
|
||||||
|
return fmt.Errorf("add static nat rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.addJumpRules(); err != nil {
|
if err := r.addJumpRules(); err != nil {
|
||||||
return fmt.Errorf("add jump rules: %w", err)
|
return fmt.Errorf("add jump rules: %w", err)
|
||||||
}
|
}
|
||||||
@@ -366,6 +380,32 @@ func (r *router) createContainers() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *router) addPostroutingRules() error {
|
||||||
|
// First rule for outbound masquerade
|
||||||
|
rule1 := []string{
|
||||||
|
"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkMasquerade),
|
||||||
|
"!", "-o", "lo",
|
||||||
|
"-j", routingFinalNatJump,
|
||||||
|
}
|
||||||
|
if err := r.iptablesClient.Append(tableNat, chainRTNAT, rule1...); err != nil {
|
||||||
|
return fmt.Errorf("add outbound masquerade rule: %v", err)
|
||||||
|
}
|
||||||
|
r.rules["static-nat-outbound"] = rule1
|
||||||
|
|
||||||
|
// Second rule for return traffic masquerade
|
||||||
|
rule2 := []string{
|
||||||
|
"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkMasqueradeReturn),
|
||||||
|
"-o", r.wgIface.Name(),
|
||||||
|
"-j", routingFinalNatJump,
|
||||||
|
}
|
||||||
|
if err := r.iptablesClient.Append(tableNat, chainRTNAT, rule2...); err != nil {
|
||||||
|
return fmt.Errorf("add return masquerade rule: %v", err)
|
||||||
|
}
|
||||||
|
r.rules["static-nat-return"] = rule2
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *router) createAndSetupChain(chain string) error {
|
func (r *router) createAndSetupChain(chain string) error {
|
||||||
table := r.getTableForChain(chain)
|
table := r.getTableForChain(chain)
|
||||||
|
|
||||||
@@ -377,10 +417,14 @@ func (r *router) createAndSetupChain(chain string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) getTableForChain(chain string) string {
|
func (r *router) getTableForChain(chain string) string {
|
||||||
if chain == chainRTNAT {
|
switch chain {
|
||||||
|
case chainRTNAT:
|
||||||
return tableNat
|
return tableNat
|
||||||
|
case chainRTPRE:
|
||||||
|
return tableMangle
|
||||||
|
default:
|
||||||
|
return tableFilter
|
||||||
}
|
}
|
||||||
return tableFilter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) insertEstablishedRule(chain string) error {
|
func (r *router) insertEstablishedRule(chain string) error {
|
||||||
@@ -398,25 +442,39 @@ func (r *router) insertEstablishedRule(chain string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) addJumpRules() error {
|
func (r *router) addJumpRules() error {
|
||||||
rule := []string{"-j", chainRTNAT}
|
// Jump to NAT chain
|
||||||
err := r.iptablesClient.Insert(tableNat, chainPOSTROUTING, 1, rule...)
|
natRule := []string{"-j", chainRTNAT}
|
||||||
if err != nil {
|
if err := r.iptablesClient.Insert(tableNat, chainPOSTROUTING, 1, natRule...); err != nil {
|
||||||
return err
|
return fmt.Errorf("add nat jump rule: %v", err)
|
||||||
}
|
}
|
||||||
r.rules[ipv4Nat] = rule
|
r.rules[jumpNat] = natRule
|
||||||
|
|
||||||
|
// Jump to prerouting chain
|
||||||
|
preRule := []string{"-j", chainRTPRE}
|
||||||
|
if err := r.iptablesClient.Insert(tableMangle, chainPREROUTING, 1, preRule...); err != nil {
|
||||||
|
return fmt.Errorf("add prerouting jump rule: %v", err)
|
||||||
|
}
|
||||||
|
r.rules[jumpPre] = preRule
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) cleanJumpRules() error {
|
func (r *router) cleanJumpRules() error {
|
||||||
rule, found := r.rules[ipv4Nat]
|
for _, ruleKey := range []string{jumpNat, jumpPre} {
|
||||||
if found {
|
if rule, exists := r.rules[ruleKey]; exists {
|
||||||
err := r.iptablesClient.DeleteIfExists(tableNat, chainPOSTROUTING, rule...)
|
table := tableNat
|
||||||
if err != nil {
|
chain := chainPOSTROUTING
|
||||||
return fmt.Errorf("failed cleaning rule from chain %s, err: %v", chainPOSTROUTING, err)
|
if ruleKey == jumpPre {
|
||||||
|
table = tableMangle
|
||||||
|
chain = chainPREROUTING
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.iptablesClient.DeleteIfExists(table, chain, rule...); err != nil {
|
||||||
|
return fmt.Errorf("delete rule from chain %s in table %s, err: %v", chain, table, err)
|
||||||
|
}
|
||||||
|
delete(r.rules, ruleKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,19 +482,35 @@ func (r *router) addNatRule(pair firewall.RouterPair) error {
|
|||||||
ruleKey := firewall.GenKey(firewall.NatFormat, pair)
|
ruleKey := firewall.GenKey(firewall.NatFormat, pair)
|
||||||
|
|
||||||
if rule, exists := r.rules[ruleKey]; exists {
|
if rule, exists := r.rules[ruleKey]; exists {
|
||||||
if err := r.iptablesClient.DeleteIfExists(tableNat, chainRTNAT, rule...); err != nil {
|
if err := r.iptablesClient.DeleteIfExists(tableMangle, chainRTPRE, rule...); err != nil {
|
||||||
return fmt.Errorf("error while removing existing NAT rule for %s: %v", pair.Destination, err)
|
return fmt.Errorf("error while removing existing marking rule for %s: %v", pair.Destination, err)
|
||||||
}
|
}
|
||||||
delete(r.rules, ruleKey)
|
delete(r.rules, ruleKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
rule := genRuleSpec(routingFinalNatJump, pair.Source, pair.Destination, r.wgIface.Name(), pair.Inverse)
|
markValue := nbnet.PreroutingFwmarkMasquerade
|
||||||
if err := r.iptablesClient.Append(tableNat, chainRTNAT, rule...); err != nil {
|
if pair.Inverse {
|
||||||
return fmt.Errorf("error while appending new NAT rule for %s: %v", pair.Destination, err)
|
markValue = nbnet.PreroutingFwmarkMasqueradeReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := []string{"-i", r.wgIface.Name()}
|
||||||
|
if pair.Inverse {
|
||||||
|
rule = []string{"!", "-i", r.wgIface.Name()}
|
||||||
|
}
|
||||||
|
|
||||||
|
rule = append(rule,
|
||||||
|
"-m", "conntrack",
|
||||||
|
"--ctstate", "NEW",
|
||||||
|
"-s", pair.Source.String(),
|
||||||
|
"-d", pair.Destination.String(),
|
||||||
|
"-j", "MARK", "--set-mark", fmt.Sprintf("%#x", markValue),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := r.iptablesClient.Append(tableMangle, chainRTPRE, rule...); err != nil {
|
||||||
|
return fmt.Errorf("error while adding marking rule for %s: %v", pair.Destination, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.rules[ruleKey] = rule
|
r.rules[ruleKey] = rule
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,13 +518,12 @@ func (r *router) removeNatRule(pair firewall.RouterPair) error {
|
|||||||
ruleKey := firewall.GenKey(firewall.NatFormat, pair)
|
ruleKey := firewall.GenKey(firewall.NatFormat, pair)
|
||||||
|
|
||||||
if rule, exists := r.rules[ruleKey]; exists {
|
if rule, exists := r.rules[ruleKey]; exists {
|
||||||
if err := r.iptablesClient.DeleteIfExists(tableNat, chainRTNAT, rule...); err != nil {
|
if err := r.iptablesClient.DeleteIfExists(tableMangle, chainRTPRE, rule...); err != nil {
|
||||||
return fmt.Errorf("error while removing existing nat rule for %s: %v", pair.Destination, err)
|
return fmt.Errorf("error while removing marking rule for %s: %v", pair.Destination, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(r.rules, ruleKey)
|
delete(r.rules, ruleKey)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("nat rule %s not found", ruleKey)
|
log.Debugf("marking rule %s not found", ruleKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -482,16 +555,6 @@ func (r *router) updateState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func genRuleSpec(jump string, source, destination netip.Prefix, intf string, inverse bool) []string {
|
|
||||||
intdir := "-i"
|
|
||||||
lointdir := "-o"
|
|
||||||
if inverse {
|
|
||||||
intdir = "-o"
|
|
||||||
lointdir = "-i"
|
|
||||||
}
|
|
||||||
return []string{intdir, intf, "!", lointdir, "lo", "-s", source.String(), "-d", destination.String(), "-j", jump}
|
|
||||||
}
|
|
||||||
|
|
||||||
func genRouteFilteringRuleSpec(params routeFilteringRuleParams) []string {
|
func genRouteFilteringRuleSpec(params routeFilteringRuleParams) []string {
|
||||||
var rule []string
|
var rule []string
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,18 @@
|
|||||||
package iptables
|
package iptables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/coreos/go-iptables/iptables"
|
"github.com/coreos/go-iptables/iptables"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/firewall/test"
|
"github.com/netbirdio/netbird/client/firewall/test"
|
||||||
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isIptablesSupported() bool {
|
func isIptablesSupported() bool {
|
||||||
@@ -34,14 +35,24 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
|
|||||||
require.NoError(t, manager.init(nil))
|
require.NoError(t, manager.init(nil))
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = manager.Reset()
|
assert.NoError(t, manager.Reset(), "shouldn't return error")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
require.Len(t, manager.rules, 2, "should have created rules map")
|
// Now 5 rules:
|
||||||
|
// 1. established rule in forward chain
|
||||||
|
// 2. jump rule to NAT chain
|
||||||
|
// 3. jump rule to PRE chain
|
||||||
|
// 4. static outbound masquerade rule
|
||||||
|
// 5. static return masquerade rule
|
||||||
|
require.Len(t, manager.rules, 5, "should have created rules map")
|
||||||
|
|
||||||
exists, err := manager.iptablesClient.Exists(tableNat, chainPOSTROUTING, manager.rules[ipv4Nat]...)
|
exists, err := manager.iptablesClient.Exists(tableNat, chainPOSTROUTING, "-j", chainRTNAT)
|
||||||
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainPOSTROUTING)
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainPOSTROUTING)
|
||||||
require.True(t, exists, "postrouting rule should exist")
|
require.True(t, exists, "postrouting jump rule should exist")
|
||||||
|
|
||||||
|
exists, err = manager.iptablesClient.Exists(tableMangle, chainPREROUTING, "-j", chainRTPRE)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableMangle, chainPREROUTING)
|
||||||
|
require.True(t, exists, "prerouting jump rule should exist")
|
||||||
|
|
||||||
pair := firewall.RouterPair{
|
pair := firewall.RouterPair{
|
||||||
ID: "abc",
|
ID: "abc",
|
||||||
@@ -49,22 +60,15 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
|
|||||||
Destination: netip.MustParsePrefix("100.100.100.0/24"),
|
Destination: netip.MustParsePrefix("100.100.100.0/24"),
|
||||||
Masquerade: true,
|
Masquerade: true,
|
||||||
}
|
}
|
||||||
forward4Rule := []string{"-s", pair.Source.String(), "-d", pair.Destination.String(), "-j", routingFinalForwardJump}
|
|
||||||
|
|
||||||
err = manager.iptablesClient.Insert(tableFilter, chainRTFWD, 1, forward4Rule...)
|
err = manager.AddNatRule(pair)
|
||||||
require.NoError(t, err, "inserting rule should not return error")
|
require.NoError(t, err, "adding NAT rule should not return error")
|
||||||
|
|
||||||
nat4Rule := genRuleSpec(routingFinalNatJump, pair.Source, pair.Destination, ifaceMock.Name(), false)
|
|
||||||
|
|
||||||
err = manager.iptablesClient.Insert(tableNat, chainRTNAT, 1, nat4Rule...)
|
|
||||||
require.NoError(t, err, "inserting rule should not return error")
|
|
||||||
|
|
||||||
err = manager.Reset()
|
err = manager.Reset()
|
||||||
require.NoError(t, err, "shouldn't return error")
|
require.NoError(t, err, "shouldn't return error")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIptablesManager_AddNatRule(t *testing.T) {
|
func TestIptablesManager_AddNatRule(t *testing.T) {
|
||||||
|
|
||||||
if !isIptablesSupported() {
|
if !isIptablesSupported() {
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
}
|
}
|
||||||
@@ -79,52 +83,66 @@ func TestIptablesManager_AddNatRule(t *testing.T) {
|
|||||||
require.NoError(t, manager.init(nil))
|
require.NoError(t, manager.init(nil))
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := manager.Reset()
|
assert.NoError(t, manager.Reset(), "shouldn't return error")
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to reset iptables manager: %s", err)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = manager.AddNatRule(testCase.InputPair)
|
err = manager.AddNatRule(testCase.InputPair)
|
||||||
require.NoError(t, err, "forwarding pair should be inserted")
|
require.NoError(t, err, "marking rule should be inserted")
|
||||||
|
|
||||||
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair)
|
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair)
|
||||||
natRule := genRuleSpec(routingFinalNatJump, testCase.InputPair.Source, testCase.InputPair.Destination, ifaceMock.Name(), false)
|
markingRule := []string{
|
||||||
|
"-i", ifaceMock.Name(),
|
||||||
exists, err := iptablesClient.Exists(tableNat, chainRTNAT, natRule...)
|
"-m", "conntrack",
|
||||||
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainRTNAT)
|
"--ctstate", "NEW",
|
||||||
if testCase.InputPair.Masquerade {
|
"-s", testCase.InputPair.Source.String(),
|
||||||
require.True(t, exists, "nat rule should be created")
|
"-d", testCase.InputPair.Destination.String(),
|
||||||
foundNatRule, foundNat := manager.rules[natRuleKey]
|
"-j", "MARK", "--set-mark",
|
||||||
require.True(t, foundNat, "nat rule should exist in the map")
|
fmt.Sprintf("%#x", nbnet.PreroutingFwmarkMasquerade),
|
||||||
require.Equal(t, natRule[:4], foundNatRule[:4], "stored nat rule should match")
|
|
||||||
} else {
|
|
||||||
require.False(t, exists, "nat rule should not be created")
|
|
||||||
_, foundNat := manager.rules[natRuleKey]
|
|
||||||
require.False(t, foundNat, "nat rule should not exist in the map")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inNatRuleKey := firewall.GenKey(firewall.NatFormat, firewall.GetInversePair(testCase.InputPair))
|
exists, err := iptablesClient.Exists(tableMangle, chainRTPRE, markingRule...)
|
||||||
inNatRule := genRuleSpec(routingFinalNatJump, firewall.GetInversePair(testCase.InputPair).Source, firewall.GetInversePair(testCase.InputPair).Destination, ifaceMock.Name(), true)
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableMangle, chainRTPRE)
|
||||||
|
|
||||||
exists, err = iptablesClient.Exists(tableNat, chainRTNAT, inNatRule...)
|
|
||||||
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainRTNAT)
|
|
||||||
if testCase.InputPair.Masquerade {
|
if testCase.InputPair.Masquerade {
|
||||||
require.True(t, exists, "income nat rule should be created")
|
require.True(t, exists, "marking rule should be created")
|
||||||
foundNatRule, foundNat := manager.rules[inNatRuleKey]
|
foundRule, found := manager.rules[natRuleKey]
|
||||||
require.True(t, foundNat, "income nat rule should exist in the map")
|
require.True(t, found, "marking rule should exist in the map")
|
||||||
require.Equal(t, inNatRule[:4], foundNatRule[:4], "stored income nat rule should match")
|
require.Equal(t, markingRule, foundRule, "stored marking rule should match")
|
||||||
} else {
|
} else {
|
||||||
require.False(t, exists, "nat rule should not be created")
|
require.False(t, exists, "marking rule should not be created")
|
||||||
_, foundNat := manager.rules[inNatRuleKey]
|
_, found := manager.rules[natRuleKey]
|
||||||
require.False(t, foundNat, "income nat rule should not exist in the map")
|
require.False(t, found, "marking rule should not exist in the map")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check inverse rule
|
||||||
|
inversePair := firewall.GetInversePair(testCase.InputPair)
|
||||||
|
inverseRuleKey := firewall.GenKey(firewall.NatFormat, inversePair)
|
||||||
|
inverseMarkingRule := []string{
|
||||||
|
"!", "-i", ifaceMock.Name(),
|
||||||
|
"-m", "conntrack",
|
||||||
|
"--ctstate", "NEW",
|
||||||
|
"-s", inversePair.Source.String(),
|
||||||
|
"-d", inversePair.Destination.String(),
|
||||||
|
"-j", "MARK", "--set-mark",
|
||||||
|
fmt.Sprintf("%#x", nbnet.PreroutingFwmarkMasqueradeReturn),
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(tableMangle, chainRTPRE, inverseMarkingRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableMangle, chainRTPRE)
|
||||||
|
if testCase.InputPair.Masquerade {
|
||||||
|
require.True(t, exists, "inverse marking rule should be created")
|
||||||
|
foundRule, found := manager.rules[inverseRuleKey]
|
||||||
|
require.True(t, found, "inverse marking rule should exist in the map")
|
||||||
|
require.Equal(t, inverseMarkingRule, foundRule, "stored inverse marking rule should match")
|
||||||
|
} else {
|
||||||
|
require.False(t, exists, "inverse marking rule should not be created")
|
||||||
|
_, found := manager.rules[inverseRuleKey]
|
||||||
|
require.False(t, found, "inverse marking rule should not exist in the map")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIptablesManager_RemoveNatRule(t *testing.T) {
|
func TestIptablesManager_RemoveNatRule(t *testing.T) {
|
||||||
|
|
||||||
if !isIptablesSupported() {
|
if !isIptablesSupported() {
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
}
|
}
|
||||||
@@ -137,42 +155,52 @@ func TestIptablesManager_RemoveNatRule(t *testing.T) {
|
|||||||
require.NoError(t, err, "shouldn't return error")
|
require.NoError(t, err, "shouldn't return error")
|
||||||
require.NoError(t, manager.init(nil))
|
require.NoError(t, manager.init(nil))
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = manager.Reset()
|
assert.NoError(t, manager.Reset(), "shouldn't return error")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
require.NoError(t, err, "shouldn't return error")
|
err = manager.AddNatRule(testCase.InputPair)
|
||||||
|
require.NoError(t, err, "should add NAT rule without error")
|
||||||
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair)
|
|
||||||
natRule := genRuleSpec(routingFinalNatJump, testCase.InputPair.Source, testCase.InputPair.Destination, ifaceMock.Name(), false)
|
|
||||||
|
|
||||||
err = iptablesClient.Insert(tableNat, chainRTNAT, 1, natRule...)
|
|
||||||
require.NoError(t, err, "inserting rule should not return error")
|
|
||||||
|
|
||||||
inNatRuleKey := firewall.GenKey(firewall.NatFormat, firewall.GetInversePair(testCase.InputPair))
|
|
||||||
inNatRule := genRuleSpec(routingFinalNatJump, firewall.GetInversePair(testCase.InputPair).Source, firewall.GetInversePair(testCase.InputPair).Destination, ifaceMock.Name(), true)
|
|
||||||
|
|
||||||
err = iptablesClient.Insert(tableNat, chainRTNAT, 1, inNatRule...)
|
|
||||||
require.NoError(t, err, "inserting rule should not return error")
|
|
||||||
|
|
||||||
err = manager.Reset()
|
|
||||||
require.NoError(t, err, "shouldn't return error")
|
|
||||||
|
|
||||||
err = manager.RemoveNatRule(testCase.InputPair)
|
err = manager.RemoveNatRule(testCase.InputPair)
|
||||||
require.NoError(t, err, "shouldn't return error")
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
exists, err := iptablesClient.Exists(tableNat, chainRTNAT, natRule...)
|
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair)
|
||||||
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainRTNAT)
|
markingRule := []string{
|
||||||
require.False(t, exists, "nat rule should not exist")
|
"-i", ifaceMock.Name(),
|
||||||
|
"-m", "conntrack",
|
||||||
|
"--ctstate", "NEW",
|
||||||
|
"-s", testCase.InputPair.Source.String(),
|
||||||
|
"-d", testCase.InputPair.Destination.String(),
|
||||||
|
"-j", "MARK", "--set-mark",
|
||||||
|
fmt.Sprintf("%#x", nbnet.PreroutingFwmarkMasquerade),
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := iptablesClient.Exists(tableMangle, chainRTPRE, markingRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableMangle, chainRTPRE)
|
||||||
|
require.False(t, exists, "marking rule should not exist")
|
||||||
|
|
||||||
_, found := manager.rules[natRuleKey]
|
_, found := manager.rules[natRuleKey]
|
||||||
require.False(t, found, "nat rule should exist in the manager map")
|
require.False(t, found, "marking rule should not exist in the manager map")
|
||||||
|
|
||||||
exists, err = iptablesClient.Exists(tableNat, chainRTNAT, inNatRule...)
|
// Check inverse rule removal
|
||||||
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainRTNAT)
|
inversePair := firewall.GetInversePair(testCase.InputPair)
|
||||||
require.False(t, exists, "income nat rule should not exist")
|
inverseRuleKey := firewall.GenKey(firewall.NatFormat, inversePair)
|
||||||
|
inverseMarkingRule := []string{
|
||||||
|
"!", "-i", ifaceMock.Name(),
|
||||||
|
"-m", "conntrack",
|
||||||
|
"--ctstate", "NEW",
|
||||||
|
"-s", inversePair.Source.String(),
|
||||||
|
"-d", inversePair.Destination.String(),
|
||||||
|
"-j", "MARK", "--set-mark",
|
||||||
|
fmt.Sprintf("%#x", nbnet.PreroutingFwmarkMasqueradeReturn),
|
||||||
|
}
|
||||||
|
|
||||||
_, found = manager.rules[inNatRuleKey]
|
exists, err = iptablesClient.Exists(tableMangle, chainRTPRE, inverseMarkingRule...)
|
||||||
require.False(t, found, "income nat rule should exist in the manager map")
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableMangle, chainRTPRE)
|
||||||
|
require.False(t, exists, "inverse marking rule should not exist")
|
||||||
|
|
||||||
|
_, found = manager.rules[inverseRuleKey]
|
||||||
|
require.False(t, found, "inverse marking rule should not exist in the map")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ func (s *ipList) UnmarshalJSON(data []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.ips = temp.IPs
|
s.ips = temp.IPs
|
||||||
|
|
||||||
|
if temp.IPs == nil {
|
||||||
|
temp.IPs = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,5 +94,10 @@ func (s *ipsetStore) UnmarshalJSON(data []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.ipsets = temp.IPSets
|
s.ipsets = temp.IPSets
|
||||||
|
|
||||||
|
if temp.IPSets == nil {
|
||||||
|
temp.IPSets = make(map[string]*ipList)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
ForwardingFormatPrefix = "netbird-fwd-"
|
ForwardingFormatPrefix = "netbird-fwd-"
|
||||||
ForwardingFormat = "netbird-fwd-%s-%t"
|
ForwardingFormat = "netbird-fwd-%s-%t"
|
||||||
|
PreroutingFormat = "netbird-prerouting-%s-%t"
|
||||||
NatFormat = "netbird-nat-%s-%t"
|
NatFormat = "netbird-nat-%s-%t"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ func (m *AclManager) addPreroutingRule(preroutingChain *nftables.Chain) {
|
|||||||
},
|
},
|
||||||
&expr.Immediate{
|
&expr.Immediate{
|
||||||
Register: 1,
|
Register: 1,
|
||||||
Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmark),
|
Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkRedirected),
|
||||||
},
|
},
|
||||||
&expr.Meta{
|
&expr.Meta{
|
||||||
Key: expr.MetaKeyMARK,
|
Key: expr.MetaKeyMARK,
|
||||||
@@ -543,7 +543,7 @@ func (m *AclManager) addFwmarkToForward(chainFwFilter *nftables.Chain) {
|
|||||||
&expr.Cmp{
|
&expr.Cmp{
|
||||||
Op: expr.CmpOpEq,
|
Op: expr.CmpOpEq,
|
||||||
Register: 1,
|
Register: 1,
|
||||||
Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmark),
|
Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkRedirected),
|
||||||
},
|
},
|
||||||
&expr.Verdict{
|
&expr.Verdict{
|
||||||
Kind: expr.VerdictJump,
|
Kind: expr.VerdictJump,
|
||||||
|
|||||||
@@ -99,9 +99,11 @@ func (m *Manager) Init(stateManager *statemanager.Manager) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// persist early
|
// persist early
|
||||||
if err := stateManager.PersistState(context.Background()); err != nil {
|
go func() {
|
||||||
log.Errorf("failed to persist state: %v", err)
|
if err := stateManager.PersistState(context.Background()); err != nil {
|
||||||
}
|
log.Errorf("failed to persist state: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -197,7 +199,7 @@ func (m *Manager) AllowNetbird() error {
|
|||||||
|
|
||||||
var chain *nftables.Chain
|
var chain *nftables.Chain
|
||||||
for _, c := range chains {
|
for _, c := range chains {
|
||||||
if c.Table.Name == tableNameFilter && c.Name == chainNameForward {
|
if c.Table.Name == tableNameFilter && c.Name == chainNameInput {
|
||||||
chain = c
|
chain = c
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -274,7 +276,7 @@ func (m *Manager) resetNetbirdInputRules() error {
|
|||||||
|
|
||||||
func (m *Manager) deleteNetbirdInputRules(chains []*nftables.Chain) {
|
func (m *Manager) deleteNetbirdInputRules(chains []*nftables.Chain) {
|
||||||
for _, c := range chains {
|
for _, c := range chains {
|
||||||
if c.Table.Name == "filter" && c.Name == "INPUT" {
|
if c.Table.Name == tableNameFilter && c.Name == chainNameInput {
|
||||||
rules, err := m.rConn.GetRules(c.Table, c)
|
rules, err := m.rConn.GetRules(c.Table, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("get rules for chain %q: %v", c.Name, err)
|
log.Errorf("get rules for chain %q: %v", c.Name, err)
|
||||||
@@ -349,7 +351,9 @@ func (m *Manager) applyAllowNetbirdRules(chain *nftables.Chain) {
|
|||||||
Register: 1,
|
Register: 1,
|
||||||
Data: ifname(m.wgIface.Name()),
|
Data: ifname(m.wgIface.Name()),
|
||||||
},
|
},
|
||||||
&expr.Verdict{},
|
&expr.Verdict{
|
||||||
|
Kind: expr.VerdictAccept,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
UserData: []byte(allowNetbirdInputRuleID),
|
UserData: []byte(allowNetbirdInputRuleID),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package nftables
|
package nftables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os/exec"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -225,3 +227,105 @@ func TestNFtablesCreatePerformance(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runIptablesSave(t *testing.T) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd := exec.Command("iptables-save")
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
require.NoError(t, err, "iptables-save failed to run")
|
||||||
|
|
||||||
|
return stdout.String(), stderr.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyIptablesOutput(t *testing.T, stdout, stderr string) {
|
||||||
|
t.Helper()
|
||||||
|
// Check for any incompatibility warnings
|
||||||
|
require.NotContains(t,
|
||||||
|
stderr,
|
||||||
|
"incompatible",
|
||||||
|
"iptables-save produced compatibility warning. Full stderr: %s",
|
||||||
|
stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify standard tables are present
|
||||||
|
expectedTables := []string{
|
||||||
|
"*filter",
|
||||||
|
"*nat",
|
||||||
|
"*mangle",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range expectedTables {
|
||||||
|
require.Contains(t,
|
||||||
|
stdout,
|
||||||
|
table,
|
||||||
|
"iptables-save output missing expected table: %s\nFull stdout: %s",
|
||||||
|
table,
|
||||||
|
stdout,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
|
||||||
|
if check() != NFTABLES {
|
||||||
|
t.Skip("nftables not supported on this system")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := exec.LookPath("iptables-save"); err != nil {
|
||||||
|
t.Skipf("iptables-save not available on this system: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First ensure iptables-nft tables exist by running iptables-save
|
||||||
|
stdout, stderr := runIptablesSave(t)
|
||||||
|
verifyIptablesOutput(t, stdout, stderr)
|
||||||
|
|
||||||
|
manager, err := Create(ifaceMock)
|
||||||
|
require.NoError(t, err, "failed to create manager")
|
||||||
|
require.NoError(t, manager.Init(nil))
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := manager.Reset(nil)
|
||||||
|
require.NoError(t, err, "failed to reset manager state")
|
||||||
|
|
||||||
|
// Verify iptables output after reset
|
||||||
|
stdout, stderr := runIptablesSave(t)
|
||||||
|
verifyIptablesOutput(t, stdout, stderr)
|
||||||
|
})
|
||||||
|
|
||||||
|
ip := net.ParseIP("100.96.0.1")
|
||||||
|
_, err = manager.AddPeerFiltering(
|
||||||
|
ip,
|
||||||
|
fw.ProtocolTCP,
|
||||||
|
nil,
|
||||||
|
&fw.Port{Values: []int{80}},
|
||||||
|
fw.RuleDirectionIN,
|
||||||
|
fw.ActionAccept,
|
||||||
|
"",
|
||||||
|
"test rule",
|
||||||
|
)
|
||||||
|
require.NoError(t, err, "failed to add peer filtering rule")
|
||||||
|
|
||||||
|
_, err = manager.AddRouteFiltering(
|
||||||
|
[]netip.Prefix{netip.MustParsePrefix("192.168.2.0/24")},
|
||||||
|
netip.MustParsePrefix("10.1.0.0/24"),
|
||||||
|
fw.ProtocolTCP,
|
||||||
|
nil,
|
||||||
|
&fw.Port{Values: []int{443}},
|
||||||
|
fw.ActionAccept,
|
||||||
|
)
|
||||||
|
require.NoError(t, err, "failed to add route filtering rule")
|
||||||
|
|
||||||
|
pair := fw.RouterPair{
|
||||||
|
Source: netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
|
Destination: netip.MustParsePrefix("10.0.0.0/24"),
|
||||||
|
Masquerade: true,
|
||||||
|
}
|
||||||
|
err = manager.AddNatRule(pair)
|
||||||
|
require.NoError(t, err, "failed to add NAT rule")
|
||||||
|
|
||||||
|
stdout, stderr = runIptablesSave(t)
|
||||||
|
verifyIptablesOutput(t, stdout, stderr)
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/internal/acl/id"
|
"github.com/netbirdio/netbird/client/internal/acl/id"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -124,7 +125,6 @@ func (r *router) createContainers() error {
|
|||||||
insertReturnTrafficRule(r.conn, r.workTable, r.chains[chainNameRoutingFw])
|
insertReturnTrafficRule(r.conn, r.workTable, r.chains[chainNameRoutingFw])
|
||||||
|
|
||||||
prio := *nftables.ChainPriorityNATSource - 1
|
prio := *nftables.ChainPriorityNATSource - 1
|
||||||
|
|
||||||
r.chains[chainNameRoutingNat] = r.conn.AddChain(&nftables.Chain{
|
r.chains[chainNameRoutingNat] = r.conn.AddChain(&nftables.Chain{
|
||||||
Name: chainNameRoutingNat,
|
Name: chainNameRoutingNat,
|
||||||
Table: r.workTable,
|
Table: r.workTable,
|
||||||
@@ -133,6 +133,21 @@ func (r *router) createContainers() error {
|
|||||||
Type: nftables.ChainTypeNAT,
|
Type: nftables.ChainTypeNAT,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Chain is created by acl manager
|
||||||
|
// TODO: move creation to a common place
|
||||||
|
r.chains[chainNamePrerouting] = &nftables.Chain{
|
||||||
|
Name: chainNamePrerouting,
|
||||||
|
Table: r.workTable,
|
||||||
|
Type: nftables.ChainTypeFilter,
|
||||||
|
Hooknum: nftables.ChainHookPrerouting,
|
||||||
|
Priority: nftables.ChainPriorityMangle,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the single NAT rule that matches on mark
|
||||||
|
if err := r.addPostroutingRules(); err != nil {
|
||||||
|
return fmt.Errorf("add single nat rule: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.acceptForwardRules(); err != nil {
|
if err := r.acceptForwardRules(); err != nil {
|
||||||
log.Errorf("failed to add accept rules for the forward chain: %s", err)
|
log.Errorf("failed to add accept rules for the forward chain: %s", err)
|
||||||
}
|
}
|
||||||
@@ -422,59 +437,149 @@ func (r *router) addNatRule(pair firewall.RouterPair) error {
|
|||||||
sourceExp := generateCIDRMatcherExpressions(true, pair.Source)
|
sourceExp := generateCIDRMatcherExpressions(true, pair.Source)
|
||||||
destExp := generateCIDRMatcherExpressions(false, pair.Destination)
|
destExp := generateCIDRMatcherExpressions(false, pair.Destination)
|
||||||
|
|
||||||
dir := expr.MetaKeyIIFNAME
|
op := expr.CmpOpEq
|
||||||
notDir := expr.MetaKeyOIFNAME
|
|
||||||
if pair.Inverse {
|
if pair.Inverse {
|
||||||
dir = expr.MetaKeyOIFNAME
|
op = expr.CmpOpNeq
|
||||||
notDir = expr.MetaKeyIIFNAME
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lo := ifname("lo")
|
|
||||||
intf := ifname(r.wgIface.Name())
|
|
||||||
|
|
||||||
exprs := []expr.Any{
|
exprs := []expr.Any{
|
||||||
&expr.Meta{
|
// We only care about NEW connections to mark them and later identify them in the postrouting chain for masquerading.
|
||||||
Key: dir,
|
// Masquerading will take care of the conntrack state, which means we won't need to mark established connections.
|
||||||
|
&expr.Ct{
|
||||||
|
Key: expr.CtKeySTATE,
|
||||||
Register: 1,
|
Register: 1,
|
||||||
},
|
},
|
||||||
&expr.Cmp{
|
&expr.Bitwise{
|
||||||
Op: expr.CmpOpEq,
|
SourceRegister: 1,
|
||||||
Register: 1,
|
DestRegister: 1,
|
||||||
Data: intf,
|
Len: 4,
|
||||||
},
|
Mask: binaryutil.NativeEndian.PutUint32(expr.CtStateBitNEW),
|
||||||
|
Xor: binaryutil.NativeEndian.PutUint32(0),
|
||||||
// We need to exclude the loopback interface as this changes the ebpf proxy port
|
|
||||||
&expr.Meta{
|
|
||||||
Key: notDir,
|
|
||||||
Register: 1,
|
|
||||||
},
|
},
|
||||||
&expr.Cmp{
|
&expr.Cmp{
|
||||||
Op: expr.CmpOpNeq,
|
Op: expr.CmpOpNeq,
|
||||||
Register: 1,
|
Register: 1,
|
||||||
Data: lo,
|
Data: []byte{0, 0, 0, 0},
|
||||||
|
},
|
||||||
|
|
||||||
|
// interface matching
|
||||||
|
&expr.Meta{
|
||||||
|
Key: expr.MetaKeyIIFNAME,
|
||||||
|
Register: 1,
|
||||||
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: op,
|
||||||
|
Register: 1,
|
||||||
|
Data: ifname(r.wgIface.Name()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
exprs = append(exprs, sourceExp...)
|
exprs = append(exprs, sourceExp...)
|
||||||
exprs = append(exprs, destExp...)
|
exprs = append(exprs, destExp...)
|
||||||
|
|
||||||
|
var markValue uint32 = nbnet.PreroutingFwmarkMasquerade
|
||||||
|
if pair.Inverse {
|
||||||
|
markValue = nbnet.PreroutingFwmarkMasqueradeReturn
|
||||||
|
}
|
||||||
|
|
||||||
exprs = append(exprs,
|
exprs = append(exprs,
|
||||||
&expr.Counter{}, &expr.Masq{},
|
&expr.Immediate{
|
||||||
|
Register: 1,
|
||||||
|
Data: binaryutil.NativeEndian.PutUint32(markValue),
|
||||||
|
},
|
||||||
|
&expr.Meta{
|
||||||
|
Key: expr.MetaKeyMARK,
|
||||||
|
SourceRegister: true,
|
||||||
|
Register: 1,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
ruleKey := firewall.GenKey(firewall.NatFormat, pair)
|
ruleKey := firewall.GenKey(firewall.PreroutingFormat, pair)
|
||||||
|
|
||||||
if _, exists := r.rules[ruleKey]; exists {
|
if _, exists := r.rules[ruleKey]; exists {
|
||||||
if err := r.removeNatRule(pair); err != nil {
|
if err := r.removeNatRule(pair); err != nil {
|
||||||
return fmt.Errorf("remove routing rule: %w", err)
|
return fmt.Errorf("remove prerouting rule: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.rules[ruleKey] = r.conn.AddRule(&nftables.Rule{
|
r.rules[ruleKey] = r.conn.AddRule(&nftables.Rule{
|
||||||
Table: r.workTable,
|
Table: r.workTable,
|
||||||
Chain: r.chains[chainNameRoutingNat],
|
Chain: r.chains[chainNamePrerouting],
|
||||||
Exprs: exprs,
|
Exprs: exprs,
|
||||||
UserData: []byte(ruleKey),
|
UserData: []byte(ruleKey),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addPostroutingRules adds the masquerade rules
|
||||||
|
func (r *router) addPostroutingRules() error {
|
||||||
|
// First masquerade rule for traffic coming in from WireGuard interface
|
||||||
|
exprs := []expr.Any{
|
||||||
|
// Match on the first fwmark
|
||||||
|
&expr.Meta{
|
||||||
|
Key: expr.MetaKeyMARK,
|
||||||
|
Register: 1,
|
||||||
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 1,
|
||||||
|
Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkMasquerade),
|
||||||
|
},
|
||||||
|
|
||||||
|
// We need to exclude the loopback interface as this changes the ebpf proxy port
|
||||||
|
&expr.Meta{
|
||||||
|
Key: expr.MetaKeyOIFNAME,
|
||||||
|
Register: 1,
|
||||||
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpNeq,
|
||||||
|
Register: 1,
|
||||||
|
Data: ifname("lo"),
|
||||||
|
},
|
||||||
|
&expr.Counter{},
|
||||||
|
&expr.Masq{},
|
||||||
|
}
|
||||||
|
|
||||||
|
r.conn.AddRule(&nftables.Rule{
|
||||||
|
Table: r.workTable,
|
||||||
|
Chain: r.chains[chainNameRoutingNat],
|
||||||
|
Exprs: exprs,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Second masquerade rule for traffic going out through WireGuard interface
|
||||||
|
exprs2 := []expr.Any{
|
||||||
|
// Match on the second fwmark
|
||||||
|
&expr.Meta{
|
||||||
|
Key: expr.MetaKeyMARK,
|
||||||
|
Register: 1,
|
||||||
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 1,
|
||||||
|
Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkMasqueradeReturn),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Match WireGuard interface
|
||||||
|
&expr.Meta{
|
||||||
|
Key: expr.MetaKeyOIFNAME,
|
||||||
|
Register: 1,
|
||||||
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 1,
|
||||||
|
Data: ifname(r.wgIface.Name()),
|
||||||
|
},
|
||||||
|
&expr.Counter{},
|
||||||
|
&expr.Masq{},
|
||||||
|
}
|
||||||
|
|
||||||
|
r.conn.AddRule(&nftables.Rule{
|
||||||
|
Table: r.workTable,
|
||||||
|
Chain: r.chains[chainNameRoutingNat],
|
||||||
|
Exprs: exprs2,
|
||||||
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,18 +828,18 @@ func (r *router) removeAcceptForwardRulesIptables(ipt *iptables.IPTables) error
|
|||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveNatRule removes a nftables rule pair from nat chains
|
// RemoveNatRule removes the prerouting mark rule
|
||||||
func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
|
func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
|
||||||
if err := r.refreshRulesMap(); err != nil {
|
if err := r.refreshRulesMap(); err != nil {
|
||||||
return fmt.Errorf(refreshRulesMapError, err)
|
return fmt.Errorf(refreshRulesMapError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.removeNatRule(pair); err != nil {
|
if err := r.removeNatRule(pair); err != nil {
|
||||||
return fmt.Errorf("remove nat rule: %w", err)
|
return fmt.Errorf("remove prerouting rule: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
|
if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
|
||||||
return fmt.Errorf("remove inverse nat rule: %w", err)
|
return fmt.Errorf("remove inverse prerouting rule: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.removeLegacyRouteRule(pair); err != nil {
|
if err := r.removeLegacyRouteRule(pair); err != nil {
|
||||||
@@ -749,21 +854,20 @@ func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeNatRule adds a nftables rule to the removal queue and deletes it from the rules map
|
|
||||||
func (r *router) removeNatRule(pair firewall.RouterPair) error {
|
func (r *router) removeNatRule(pair firewall.RouterPair) error {
|
||||||
ruleKey := firewall.GenKey(firewall.NatFormat, pair)
|
ruleKey := firewall.GenKey(firewall.PreroutingFormat, pair)
|
||||||
|
|
||||||
if rule, exists := r.rules[ruleKey]; exists {
|
if rule, exists := r.rules[ruleKey]; exists {
|
||||||
err := r.conn.DelRule(rule)
|
err := r.conn.DelRule(rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("remove nat rule %s -> %s: %v", pair.Source, pair.Destination, err)
|
return fmt.Errorf("remove prerouting rule %s -> %s: %v", pair.Source, pair.Destination, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("nftables: removed nat rule %s -> %s", pair.Source, pair.Destination)
|
log.Debugf("nftables: removed prerouting rule %s -> %s", pair.Source, pair.Destination)
|
||||||
|
|
||||||
delete(r.rules, ruleKey)
|
delete(r.rules, ruleKey)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("nftables: nat rule %s not found", ruleKey)
|
log.Debugf("nftables: prerouting rule %s not found", ruleKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/coreos/go-iptables/iptables"
|
"github.com/coreos/go-iptables/iptables"
|
||||||
"github.com/google/nftables"
|
"github.com/google/nftables"
|
||||||
|
"github.com/google/nftables/binaryutil"
|
||||||
"github.com/google/nftables/expr"
|
"github.com/google/nftables/expr"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -32,100 +33,87 @@ func TestNftablesManager_AddNatRule(t *testing.T) {
|
|||||||
t.Skip("nftables not supported on this OS")
|
t.Skip("nftables not supported on this OS")
|
||||||
}
|
}
|
||||||
|
|
||||||
table, err := createWorkTable()
|
|
||||||
require.NoError(t, err, "Failed to create work table")
|
|
||||||
|
|
||||||
defer deleteWorkTable()
|
|
||||||
|
|
||||||
for _, testCase := range test.InsertRuleTestCases {
|
for _, testCase := range test.InsertRuleTestCases {
|
||||||
t.Run(testCase.Name, func(t *testing.T) {
|
t.Run(testCase.Name, func(t *testing.T) {
|
||||||
manager, err := newRouter(table, ifaceMock)
|
// need fw manager to init both acl mgr and router for all chains to be present
|
||||||
require.NoError(t, err, "failed to create router")
|
manager, err := Create(ifaceMock)
|
||||||
require.NoError(t, manager.init(table))
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, manager.Reset(nil))
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, manager.Init(nil))
|
||||||
|
|
||||||
nftablesTestingClient := &nftables.Conn{}
|
nftablesTestingClient := &nftables.Conn{}
|
||||||
|
|
||||||
defer func(manager *router) {
|
rtr := manager.router
|
||||||
require.NoError(t, manager.Reset(), "failed to reset rules")
|
err = rtr.AddNatRule(testCase.InputPair)
|
||||||
}(manager)
|
|
||||||
|
|
||||||
require.NoError(t, err, "shouldn't return error")
|
|
||||||
|
|
||||||
err = manager.AddNatRule(testCase.InputPair)
|
|
||||||
require.NoError(t, err, "pair should be inserted")
|
require.NoError(t, err, "pair should be inserted")
|
||||||
|
|
||||||
defer func(manager *router, pair firewall.RouterPair) {
|
t.Cleanup(func() {
|
||||||
require.NoError(t, manager.RemoveNatRule(pair), "failed to remove rule")
|
require.NoError(t, rtr.RemoveNatRule(testCase.InputPair), "failed to remove rule")
|
||||||
}(manager, testCase.InputPair)
|
})
|
||||||
|
|
||||||
if testCase.InputPair.Masquerade {
|
if testCase.InputPair.Masquerade {
|
||||||
sourceExp := generateCIDRMatcherExpressions(true, testCase.InputPair.Source)
|
// Build expected expressions for connection tracking
|
||||||
destExp := generateCIDRMatcherExpressions(false, testCase.InputPair.Destination)
|
conntrackExprs := []expr.Any{
|
||||||
testingExpression := append(sourceExp, destExp...) //nolint:gocritic
|
&expr.Ct{
|
||||||
testingExpression = append(testingExpression,
|
Key: expr.CtKeySTATE,
|
||||||
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
|
Register: 1,
|
||||||
|
},
|
||||||
|
&expr.Bitwise{
|
||||||
|
SourceRegister: 1,
|
||||||
|
DestRegister: 1,
|
||||||
|
Len: 4,
|
||||||
|
Mask: binaryutil.NativeEndian.PutUint32(expr.CtStateBitNEW),
|
||||||
|
Xor: binaryutil.NativeEndian.PutUint32(0),
|
||||||
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpNeq,
|
||||||
|
Register: 1,
|
||||||
|
Data: []byte{0, 0, 0, 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build interface matching expression
|
||||||
|
ifaceExprs := []expr.Any{
|
||||||
|
&expr.Meta{
|
||||||
|
Key: expr.MetaKeyIIFNAME,
|
||||||
|
Register: 1,
|
||||||
|
},
|
||||||
&expr.Cmp{
|
&expr.Cmp{
|
||||||
Op: expr.CmpOpEq,
|
Op: expr.CmpOpEq,
|
||||||
Register: 1,
|
Register: 1,
|
||||||
Data: ifname(ifaceMock.Name()),
|
Data: ifname(ifaceMock.Name()),
|
||||||
},
|
},
|
||||||
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpNeq,
|
|
||||||
Register: 1,
|
|
||||||
Data: ifname("lo"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair)
|
|
||||||
found := 0
|
|
||||||
for _, chain := range manager.chains {
|
|
||||||
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
|
||||||
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
|
||||||
for _, rule := range rules {
|
|
||||||
if len(rule.UserData) > 0 && string(rule.UserData) == natRuleKey {
|
|
||||||
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "nat rule elements should match")
|
|
||||||
found = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
|
||||||
}
|
|
||||||
|
|
||||||
if testCase.InputPair.Masquerade {
|
// Build CIDR matching expressions
|
||||||
sourceExp := generateCIDRMatcherExpressions(true, testCase.InputPair.Source)
|
sourceExp := generateCIDRMatcherExpressions(true, testCase.InputPair.Source)
|
||||||
destExp := generateCIDRMatcherExpressions(false, testCase.InputPair.Destination)
|
destExp := generateCIDRMatcherExpressions(false, testCase.InputPair.Destination)
|
||||||
testingExpression := append(sourceExp, destExp...) //nolint:gocritic
|
|
||||||
testingExpression = append(testingExpression,
|
|
||||||
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: ifname(ifaceMock.Name()),
|
|
||||||
},
|
|
||||||
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpNeq,
|
|
||||||
Register: 1,
|
|
||||||
Data: ifname("lo"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
inNatRuleKey := firewall.GenKey(firewall.NatFormat, firewall.GetInversePair(testCase.InputPair))
|
// Combine all expressions in the correct order
|
||||||
|
// nolint:gocritic
|
||||||
|
testingExpression := append(conntrackExprs, ifaceExprs...)
|
||||||
|
testingExpression = append(testingExpression, sourceExp...)
|
||||||
|
testingExpression = append(testingExpression, destExp...)
|
||||||
|
|
||||||
|
natRuleKey := firewall.GenKey(firewall.PreroutingFormat, testCase.InputPair)
|
||||||
found := 0
|
found := 0
|
||||||
for _, chain := range manager.chains {
|
for _, chain := range rtr.chains {
|
||||||
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
if chain.Name == chainNamePrerouting {
|
||||||
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
for _, rule := range rules {
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
if len(rule.UserData) > 0 && string(rule.UserData) == inNatRuleKey {
|
for _, rule := range rules {
|
||||||
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "income nat rule elements should match")
|
if len(rule.UserData) > 0 && string(rule.UserData) == natRuleKey {
|
||||||
found = 1
|
// Compare expressions up to the mark setting expressions
|
||||||
|
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "prerouting nat rule elements should match")
|
||||||
|
found = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
require.Equal(t, 1, found, "should find at least 1 rule in prerouting chain")
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,68 +123,66 @@ func TestNftablesManager_RemoveNatRule(t *testing.T) {
|
|||||||
t.Skip("nftables not supported on this OS")
|
t.Skip("nftables not supported on this OS")
|
||||||
}
|
}
|
||||||
|
|
||||||
table, err := createWorkTable()
|
|
||||||
require.NoError(t, err, "Failed to create work table")
|
|
||||||
|
|
||||||
defer deleteWorkTable()
|
|
||||||
|
|
||||||
for _, testCase := range test.RemoveRuleTestCases {
|
for _, testCase := range test.RemoveRuleTestCases {
|
||||||
t.Run(testCase.Name, func(t *testing.T) {
|
t.Run(testCase.Name, func(t *testing.T) {
|
||||||
manager, err := newRouter(table, ifaceMock)
|
manager, err := Create(ifaceMock)
|
||||||
require.NoError(t, err, "failed to create router")
|
t.Cleanup(func() {
|
||||||
require.NoError(t, manager.init(table))
|
require.NoError(t, manager.Reset(nil))
|
||||||
|
|
||||||
nftablesTestingClient := &nftables.Conn{}
|
|
||||||
|
|
||||||
defer func(manager *router) {
|
|
||||||
require.NoError(t, manager.Reset(), "failed to reset rules")
|
|
||||||
}(manager)
|
|
||||||
|
|
||||||
sourceExp := generateCIDRMatcherExpressions(true, testCase.InputPair.Source)
|
|
||||||
destExp := generateCIDRMatcherExpressions(false, testCase.InputPair.Destination)
|
|
||||||
|
|
||||||
natExp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) //nolint:gocritic
|
|
||||||
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair)
|
|
||||||
|
|
||||||
insertedNat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
|
||||||
Table: manager.workTable,
|
|
||||||
Chain: manager.chains[chainNameRoutingNat],
|
|
||||||
Exprs: natExp,
|
|
||||||
UserData: []byte(natRuleKey),
|
|
||||||
})
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, manager.Init(nil))
|
||||||
|
|
||||||
sourceExp = generateCIDRMatcherExpressions(true, firewall.GetInversePair(testCase.InputPair).Source)
|
rtr := manager.router
|
||||||
destExp = generateCIDRMatcherExpressions(false, firewall.GetInversePair(testCase.InputPair).Destination)
|
|
||||||
|
|
||||||
natExp = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) //nolint:gocritic
|
// First add the NAT rule using the router's method
|
||||||
inNatRuleKey := firewall.GenKey(firewall.NatFormat, firewall.GetInversePair(testCase.InputPair))
|
err = rtr.AddNatRule(testCase.InputPair)
|
||||||
|
require.NoError(t, err, "should add NAT rule")
|
||||||
|
|
||||||
insertedInNat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
// Verify the rule was added
|
||||||
Table: manager.workTable,
|
natRuleKey := firewall.GenKey(firewall.PreroutingFormat, testCase.InputPair)
|
||||||
Chain: manager.chains[chainNameRoutingNat],
|
found := false
|
||||||
Exprs: natExp,
|
rules, err := rtr.conn.GetRules(rtr.workTable, rtr.chains[chainNamePrerouting])
|
||||||
UserData: []byte(inNatRuleKey),
|
require.NoError(t, err, "should list rules")
|
||||||
})
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 && string(rule.UserData) == natRuleKey {
|
||||||
err = nftablesTestingClient.Flush()
|
found = true
|
||||||
require.NoError(t, err, "shouldn't return error")
|
break
|
||||||
|
|
||||||
err = manager.Reset()
|
|
||||||
require.NoError(t, err, "shouldn't return error")
|
|
||||||
|
|
||||||
err = manager.RemoveNatRule(testCase.InputPair)
|
|
||||||
require.NoError(t, err, "shouldn't return error")
|
|
||||||
|
|
||||||
for _, chain := range manager.chains {
|
|
||||||
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
|
||||||
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
|
||||||
for _, rule := range rules {
|
|
||||||
if len(rule.UserData) > 0 {
|
|
||||||
require.NotEqual(t, insertedNat.UserData, rule.UserData, "nat rule should not exist")
|
|
||||||
require.NotEqual(t, insertedInNat.UserData, rule.UserData, "income nat rule should not exist")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
require.True(t, found, "NAT rule should exist before removal")
|
||||||
|
|
||||||
|
// Now remove the rule
|
||||||
|
err = rtr.RemoveNatRule(testCase.InputPair)
|
||||||
|
require.NoError(t, err, "shouldn't return error when removing rule")
|
||||||
|
|
||||||
|
// Verify the rule was removed
|
||||||
|
found = false
|
||||||
|
rules, err = rtr.conn.GetRules(rtr.workTable, rtr.chains[chainNamePrerouting])
|
||||||
|
require.NoError(t, err, "should list rules after removal")
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 && string(rule.UserData) == natRuleKey {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.False(t, found, "NAT rule should not exist after removal")
|
||||||
|
|
||||||
|
// Verify the static postrouting rules still exist
|
||||||
|
rules, err = rtr.conn.GetRules(rtr.workTable, rtr.chains[chainNameRoutingNat])
|
||||||
|
require.NoError(t, err, "should list postrouting rules")
|
||||||
|
foundCounter := false
|
||||||
|
for _, rule := range rules {
|
||||||
|
for _, e := range rule.Exprs {
|
||||||
|
if _, ok := e.(*expr.Counter); ok {
|
||||||
|
foundCounter = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if foundCounter {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.True(t, foundCounter, "static postrouting rule should remain")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
package nftables
|
|
||||||
@@ -239,7 +239,7 @@ func (m *Manager) DeletePeerRule(rule firewall.Rule) error {
|
|||||||
// SetLegacyManagement doesn't need to be implemented for this manager
|
// SetLegacyManagement doesn't need to be implemented for this manager
|
||||||
func (m *Manager) SetLegacyManagement(isLegacy bool) error {
|
func (m *Manager) SetLegacyManagement(isLegacy bool) error {
|
||||||
if m.nativeFirewall == nil {
|
if m.nativeFirewall == nil {
|
||||||
return errRouteNotSupported
|
return nil
|
||||||
}
|
}
|
||||||
return m.nativeFirewall.SetLegacyManagement(isLegacy)
|
return m.nativeFirewall.SetLegacyManagement(isLegacy)
|
||||||
}
|
}
|
||||||
|
|||||||
12
client/iface/bind/control_android.go
Normal file
12
client/iface/bind/control_android.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package bind
|
||||||
|
|
||||||
|
import (
|
||||||
|
wireguard "golang.zx2c4.com/wireguard/conn"
|
||||||
|
|
||||||
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// ControlFns is not thread safe and should only be modified during init.
|
||||||
|
*wireguard.ControlFns = append(*wireguard.ControlFns, nbnet.ControlProtectSocket)
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/pion/transport/v3"
|
"github.com/pion/transport/v3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/ipv4"
|
"golang.org/x/net/ipv4"
|
||||||
|
"golang.org/x/net/ipv6"
|
||||||
wgConn "golang.zx2c4.com/wireguard/conn"
|
wgConn "golang.zx2c4.com/wireguard/conn"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,8 +25,8 @@ type receiverCreator struct {
|
|||||||
iceBind *ICEBind
|
iceBind *ICEBind
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc receiverCreator) CreateIPv4ReceiverFn(msgPool *sync.Pool, pc *ipv4.PacketConn, conn *net.UDPConn) wgConn.ReceiveFunc {
|
func (rc receiverCreator) CreateIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UDPConn, rxOffload bool, msgPool *sync.Pool) wgConn.ReceiveFunc {
|
||||||
return rc.iceBind.createIPv4ReceiverFn(msgPool, pc, conn)
|
return rc.iceBind.createIPv4ReceiverFn(pc, conn, rxOffload, msgPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ICEBind is a bind implementation with two main features:
|
// ICEBind is a bind implementation with two main features:
|
||||||
@@ -154,7 +155,7 @@ func (b *ICEBind) Send(bufs [][]byte, ep wgConn.Endpoint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ICEBind) createIPv4ReceiverFn(ipv4MsgsPool *sync.Pool, pc *ipv4.PacketConn, conn *net.UDPConn) wgConn.ReceiveFunc {
|
func (s *ICEBind) createIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UDPConn, rxOffload bool, msgsPool *sync.Pool) wgConn.ReceiveFunc {
|
||||||
s.muUDPMux.Lock()
|
s.muUDPMux.Lock()
|
||||||
defer s.muUDPMux.Unlock()
|
defer s.muUDPMux.Unlock()
|
||||||
|
|
||||||
@@ -166,16 +167,30 @@ func (s *ICEBind) createIPv4ReceiverFn(ipv4MsgsPool *sync.Pool, pc *ipv4.PacketC
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
return func(bufs [][]byte, sizes []int, eps []wgConn.Endpoint) (n int, err error) {
|
return func(bufs [][]byte, sizes []int, eps []wgConn.Endpoint) (n int, err error) {
|
||||||
msgs := ipv4MsgsPool.Get().(*[]ipv4.Message)
|
msgs := getMessages(msgsPool)
|
||||||
defer ipv4MsgsPool.Put(msgs)
|
|
||||||
for i := range bufs {
|
for i := range bufs {
|
||||||
(*msgs)[i].Buffers[0] = bufs[i]
|
(*msgs)[i].Buffers[0] = bufs[i]
|
||||||
|
(*msgs)[i].OOB = (*msgs)[i].OOB[:cap((*msgs)[i].OOB)]
|
||||||
}
|
}
|
||||||
|
defer putMessages(msgs, msgsPool)
|
||||||
var numMsgs int
|
var numMsgs int
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
||||||
numMsgs, err = pc.ReadBatch(*msgs, 0)
|
if rxOffload {
|
||||||
if err != nil {
|
readAt := len(*msgs) - (wgConn.IdealBatchSize / wgConn.UdpSegmentMaxDatagrams)
|
||||||
return 0, err
|
//nolint
|
||||||
|
numMsgs, err = pc.ReadBatch((*msgs)[readAt:], 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
numMsgs, err = wgConn.SplitCoalescedMessages(*msgs, readAt, wgConn.GetGSOSize)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
numMsgs, err = pc.ReadBatch(*msgs, 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
msg := &(*msgs)[0]
|
msg := &(*msgs)[0]
|
||||||
@@ -191,11 +206,12 @@ func (s *ICEBind) createIPv4ReceiverFn(ipv4MsgsPool *sync.Pool, pc *ipv4.PacketC
|
|||||||
// todo: handle err
|
// todo: handle err
|
||||||
ok, _ := s.filterOutStunMessages(msg.Buffers, msg.N, msg.Addr)
|
ok, _ := s.filterOutStunMessages(msg.Buffers, msg.N, msg.Addr)
|
||||||
if ok {
|
if ok {
|
||||||
sizes[i] = 0
|
continue
|
||||||
} else {
|
}
|
||||||
sizes[i] = msg.N
|
sizes[i] = msg.N
|
||||||
|
if sizes[i] == 0 {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
addrPort := msg.Addr.(*net.UDPAddr).AddrPort()
|
addrPort := msg.Addr.(*net.UDPAddr).AddrPort()
|
||||||
ep := &wgConn.StdNetEndpoint{AddrPort: addrPort} // TODO: remove allocation
|
ep := &wgConn.StdNetEndpoint{AddrPort: addrPort} // TODO: remove allocation
|
||||||
wgConn.GetSrcFromControl(msg.OOB[:msg.NN], ep)
|
wgConn.GetSrcFromControl(msg.OOB[:msg.NN], ep)
|
||||||
@@ -273,3 +289,15 @@ func fakeAddress(peerAddress *net.UDPAddr) (*net.UDPAddr, error) {
|
|||||||
}
|
}
|
||||||
return newAddr, nil
|
return newAddr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getMessages(msgsPool *sync.Pool) *[]ipv6.Message {
|
||||||
|
return msgsPool.Get().(*[]ipv6.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func putMessages(msgs *[]ipv6.Message, msgsPool *sync.Pool) {
|
||||||
|
for i := range *msgs {
|
||||||
|
(*msgs)[i].OOB = (*msgs)[i].OOB[:0]
|
||||||
|
(*msgs)[i] = ipv6.Message{Buffers: (*msgs)[i].Buffers, OOB: (*msgs)[i].OOB}
|
||||||
|
}
|
||||||
|
msgsPool.Put(msgs)
|
||||||
|
}
|
||||||
|
|||||||
@@ -162,12 +162,13 @@ func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault {
|
|||||||
params.Logger.Warn("UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead")
|
params.Logger.Warn("UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead")
|
||||||
var networks []ice.NetworkType
|
var networks []ice.NetworkType
|
||||||
switch {
|
switch {
|
||||||
case addr.IP.To4() != nil:
|
|
||||||
networks = []ice.NetworkType{ice.NetworkTypeUDP4}
|
|
||||||
|
|
||||||
case addr.IP.To16() != nil:
|
case addr.IP.To16() != nil:
|
||||||
networks = []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6}
|
networks = []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6}
|
||||||
|
|
||||||
|
case addr.IP.To4() != nil:
|
||||||
|
networks = []ice.NetworkType{ice.NetworkTypeUDP4}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
params.Logger.Errorf("LocalAddr expected IPV4 or IPV6, got %T", params.UDPConn.LocalAddr())
|
params.Logger.Errorf("LocalAddr expected IPV4 or IPV6, got %T", params.UDPConn.LocalAddr())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ import (
|
|||||||
type status int
|
type status int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultModuleDir = "/lib/modules"
|
unknown status = 1
|
||||||
unknown status = iota
|
unloaded status = 2
|
||||||
unloaded
|
unloading status = 3
|
||||||
unloading
|
loading status = 4
|
||||||
loading
|
live status = 5
|
||||||
live
|
inuse status = 6
|
||||||
inuse
|
defaultModuleDir = "/lib/modules"
|
||||||
envDisableWireGuardKernel = "NB_WG_KERNEL_DISABLED"
|
envDisableWireGuardKernel = "NB_WG_KERNEL_DISABLED"
|
||||||
)
|
)
|
||||||
|
|
||||||
type module struct {
|
type module struct {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package bind
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -94,7 +95,10 @@ func (p *ProxyBind) close() error {
|
|||||||
|
|
||||||
p.Bind.RemoveEndpoint(p.wgAddr)
|
p.Bind.RemoveEndpoint(p.wgAddr)
|
||||||
|
|
||||||
return p.remoteConn.Close()
|
if rErr := p.remoteConn.Close(); rErr != nil && !errors.Is(rErr, net.ErrClosed) {
|
||||||
|
return rErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProxyBind) proxyToLocal(ctx context.Context) {
|
func (p *ProxyBind) proxyToLocal(ctx context.Context) {
|
||||||
@@ -104,8 +108,8 @@ func (p *ProxyBind) proxyToLocal(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
buf := make([]byte, 1500)
|
|
||||||
for {
|
for {
|
||||||
|
buf := make([]byte, 1500)
|
||||||
n, err := p.remoteConn.Read(buf)
|
n, err := p.remoteConn.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func (e *ProxyWrapper) CloseConn() error {
|
|||||||
|
|
||||||
e.cancel()
|
e.cancel()
|
||||||
|
|
||||||
if err := e.remoteConn.Close(); err != nil {
|
if err := e.remoteConn.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||||
return fmt.Errorf("failed to close remote conn: %w", err)
|
return fmt.Errorf("failed to close remote conn: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ func (p *WGUDPProxy) close() error {
|
|||||||
p.cancel()
|
p.cancel()
|
||||||
|
|
||||||
var result *multierror.Error
|
var result *multierror.Error
|
||||||
if err := p.remoteConn.Close(); err != nil {
|
if err := p.remoteConn.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||||
result = multierror.Append(result, fmt.Errorf("remote conn: %s", err))
|
result = multierror.Append(result, fmt.Errorf("remote conn: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type ConfigInput struct {
|
|||||||
ManagementURL string
|
ManagementURL string
|
||||||
AdminURL string
|
AdminURL string
|
||||||
ConfigPath string
|
ConfigPath string
|
||||||
|
StateFilePath string
|
||||||
PreSharedKey *string
|
PreSharedKey *string
|
||||||
ServerSSHAllowed *bool
|
ServerSSHAllowed *bool
|
||||||
NATExternalIPs []string
|
NATExternalIPs []string
|
||||||
@@ -105,10 +106,10 @@ type Config struct {
|
|||||||
|
|
||||||
// DNSRouteInterval is the interval in which the DNS routes are updated
|
// DNSRouteInterval is the interval in which the DNS routes are updated
|
||||||
DNSRouteInterval time.Duration
|
DNSRouteInterval time.Duration
|
||||||
//Path to a certificate used for mTLS authentication
|
// Path to a certificate used for mTLS authentication
|
||||||
ClientCertPath string
|
ClientCertPath string
|
||||||
|
|
||||||
//Path to corresponding private key of ClientCertPath
|
// Path to corresponding private key of ClientCertPath
|
||||||
ClientCertKeyPath string
|
ClientCertKeyPath string
|
||||||
|
|
||||||
ClientCertKeyPair *tls.Certificate `json:"-"`
|
ClientCertKeyPair *tls.Certificate `json:"-"`
|
||||||
@@ -116,7 +117,7 @@ type Config struct {
|
|||||||
|
|
||||||
// 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) (*Config, error) {
|
func ReadConfig(configPath string) (*Config, error) {
|
||||||
if configFileIsExists(configPath) {
|
if fileExists(configPath) {
|
||||||
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)
|
||||||
@@ -149,7 +150,7 @@ func ReadConfig(configPath string) (*Config, error) {
|
|||||||
|
|
||||||
// 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 !configFileIsExists(input.ConfigPath) {
|
if !fileExists(input.ConfigPath) {
|
||||||
return nil, status.Errorf(codes.NotFound, "config file doesn't exist")
|
return nil, status.Errorf(codes.NotFound, "config file doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,13 +159,13 @@ 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 !configFileIsExists(input.ConfigPath) {
|
if !fileExists(input.ConfigPath) {
|
||||||
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 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = util.WriteJsonWithRestrictedPermission(input.ConfigPath, cfg)
|
err = util.WriteJsonWithRestrictedPermission(context.Background(), input.ConfigPath, cfg)
|
||||||
return cfg, err
|
return cfg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +186,7 @@ func CreateInMemoryConfig(input ConfigInput) (*Config, error) {
|
|||||||
|
|
||||||
// WriteOutConfig write put the prepared config to the given path
|
// WriteOutConfig write put the prepared config to the given path
|
||||||
func WriteOutConfig(path string, config *Config) error {
|
func WriteOutConfig(path string, config *Config) error {
|
||||||
return util.WriteJson(path, config)
|
return util.WriteJson(context.Background(), path, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -215,7 +216,7 @@ func update(input ConfigInput) (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if updated {
|
if updated {
|
||||||
if err := util.WriteJson(input.ConfigPath, config); err != nil {
|
if err := util.WriteJson(context.Background(), input.ConfigPath, config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,11 +473,19 @@ func isPreSharedKeyHidden(preSharedKey *string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func configFileIsExists(path string) bool {
|
func fileExists(path string) bool {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
return !os.IsNotExist(err)
|
return !os.IsNotExist(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createFile(path string) error {
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateOldManagementURL checks whether client can switch to the new Management URL with port 443 and the management domain.
|
// UpdateOldManagementURL checks whether client can switch to the new Management URL with port 443 and the management domain.
|
||||||
// If it can switch, then it updates the config and returns a new one. Otherwise, it returns the provided config.
|
// If it can switch, then it updates the config and returns a new one. Otherwise, it returns the provided config.
|
||||||
// The check is performed only for the NetBird's managed version.
|
// The check is performed only for the NetBird's managed version.
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ type ConnectClient struct {
|
|||||||
statusRecorder *peer.Status
|
statusRecorder *peer.Status
|
||||||
engine *Engine
|
engine *Engine
|
||||||
engineMutex sync.Mutex
|
engineMutex sync.Mutex
|
||||||
|
|
||||||
|
persistNetworkMap bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConnectClient(
|
func NewConnectClient(
|
||||||
@@ -89,6 +91,7 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
fileDescriptor int32,
|
fileDescriptor int32,
|
||||||
networkChangeListener listener.NetworkChangeListener,
|
networkChangeListener listener.NetworkChangeListener,
|
||||||
dnsManager dns.IosDnsManager,
|
dnsManager dns.IosDnsManager,
|
||||||
|
stateFilePath string,
|
||||||
) error {
|
) error {
|
||||||
// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
|
// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
|
||||||
debug.SetGCPercent(5)
|
debug.SetGCPercent(5)
|
||||||
@@ -97,6 +100,7 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
FileDescriptor: fileDescriptor,
|
FileDescriptor: fileDescriptor,
|
||||||
NetworkChangeListener: networkChangeListener,
|
NetworkChangeListener: networkChangeListener,
|
||||||
DnsManager: dnsManager,
|
DnsManager: dnsManager,
|
||||||
|
StateFilePath: stateFilePath,
|
||||||
}
|
}
|
||||||
return c.run(mobileDependency, nil, nil)
|
return c.run(mobileDependency, nil, nil)
|
||||||
}
|
}
|
||||||
@@ -157,7 +161,8 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold
|
|||||||
|
|
||||||
engineCtx, cancel := context.WithCancel(c.ctx)
|
engineCtx, cancel := context.WithCancel(c.ctx)
|
||||||
defer func() {
|
defer func() {
|
||||||
c.statusRecorder.MarkManagementDisconnected(state.err)
|
_, err := state.Status()
|
||||||
|
c.statusRecorder.MarkManagementDisconnected(err)
|
||||||
c.statusRecorder.CleanLocalPeerState()
|
c.statusRecorder.CleanLocalPeerState()
|
||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
@@ -207,7 +212,8 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold
|
|||||||
|
|
||||||
c.statusRecorder.MarkSignalDisconnected(nil)
|
c.statusRecorder.MarkSignalDisconnected(nil)
|
||||||
defer func() {
|
defer func() {
|
||||||
c.statusRecorder.MarkSignalDisconnected(state.err)
|
_, err := state.Status()
|
||||||
|
c.statusRecorder.MarkSignalDisconnected(err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
|
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
|
||||||
@@ -230,6 +236,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold
|
|||||||
|
|
||||||
relayURLs, token := parseRelayInfo(loginResp)
|
relayURLs, token := parseRelayInfo(loginResp)
|
||||||
relayManager := relayClient.NewManager(engineCtx, relayURLs, myPrivateKey.PublicKey().String())
|
relayManager := relayClient.NewManager(engineCtx, relayURLs, myPrivateKey.PublicKey().String())
|
||||||
|
c.statusRecorder.SetRelayMgr(relayManager)
|
||||||
if len(relayURLs) > 0 {
|
if len(relayURLs) > 0 {
|
||||||
if token != nil {
|
if token != nil {
|
||||||
if err := relayManager.UpdateToken(token); err != nil {
|
if err := relayManager.UpdateToken(token); err != nil {
|
||||||
@@ -240,9 +247,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold
|
|||||||
log.Infof("connecting to the Relay service(s): %s", strings.Join(relayURLs, ", "))
|
log.Infof("connecting to the Relay service(s): %s", strings.Join(relayURLs, ", "))
|
||||||
if err = relayManager.Serve(); err != nil {
|
if err = relayManager.Serve(); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
return wrapErr(err)
|
|
||||||
}
|
}
|
||||||
c.statusRecorder.SetRelayMgr(relayManager)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConfig := loginResp.GetPeerConfig()
|
peerConfig := loginResp.GetPeerConfig()
|
||||||
@@ -257,7 +262,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold
|
|||||||
|
|
||||||
c.engineMutex.Lock()
|
c.engineMutex.Lock()
|
||||||
c.engine = NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, probes, checks)
|
c.engine = NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, probes, checks)
|
||||||
|
c.engine.SetNetworkMapPersistence(c.persistNetworkMap)
|
||||||
c.engineMutex.Unlock()
|
c.engineMutex.Unlock()
|
||||||
|
|
||||||
if err := c.engine.Start(); err != nil {
|
if err := c.engine.Start(); err != nil {
|
||||||
@@ -335,6 +340,19 @@ func (c *ConnectClient) Engine() *Engine {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status returns the current client status
|
||||||
|
func (c *ConnectClient) Status() StatusType {
|
||||||
|
if c == nil {
|
||||||
|
return StatusIdle
|
||||||
|
}
|
||||||
|
status, err := CtxGetState(c.ctx).Status()
|
||||||
|
if err != nil {
|
||||||
|
return StatusIdle
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ConnectClient) Stop() error {
|
func (c *ConnectClient) Stop() error {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -361,6 +379,22 @@ func (c *ConnectClient) isContextCancelled() bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetNetworkMapPersistence enables or disables network map persistence.
|
||||||
|
// When enabled, the last received network map will be stored and can be retrieved
|
||||||
|
// through the Engine's getLatestNetworkMap method. When disabled, any stored
|
||||||
|
// network map will be cleared. This functionality is primarily used for debugging
|
||||||
|
// and should not be enabled during normal operation.
|
||||||
|
func (c *ConnectClient) SetNetworkMapPersistence(enabled bool) {
|
||||||
|
c.engineMutex.Lock()
|
||||||
|
c.persistNetworkMap = enabled
|
||||||
|
c.engineMutex.Unlock()
|
||||||
|
|
||||||
|
engine := c.Engine()
|
||||||
|
if engine != nil {
|
||||||
|
engine.SetNetworkMapPersistence(enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// createEngineConfig converts configuration received from Management Service to EngineConfig
|
// createEngineConfig converts configuration received from Management Service to EngineConfig
|
||||||
func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) {
|
func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) {
|
||||||
nm := false
|
nm := false
|
||||||
|
|||||||
222
client/internal/dns/handler_chain.go
Normal file
222
client/internal/dns/handler_chain.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PriorityDNSRoute = 100
|
||||||
|
PriorityMatchDomain = 50
|
||||||
|
PriorityDefault = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubdomainMatcher interface {
|
||||||
|
dns.Handler
|
||||||
|
MatchSubdomains() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type HandlerEntry struct {
|
||||||
|
Handler dns.Handler
|
||||||
|
Priority int
|
||||||
|
Pattern string
|
||||||
|
OrigPattern string
|
||||||
|
IsWildcard bool
|
||||||
|
StopHandler handlerWithStop
|
||||||
|
MatchSubdomains bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlerChain represents a prioritized chain of DNS handlers
|
||||||
|
type HandlerChain struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
handlers []HandlerEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseWriterChain wraps a dns.ResponseWriter to track if handler wants to continue chain
|
||||||
|
type ResponseWriterChain struct {
|
||||||
|
dns.ResponseWriter
|
||||||
|
origPattern string
|
||||||
|
shouldContinue bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ResponseWriterChain) WriteMsg(m *dns.Msg) error {
|
||||||
|
// Check if this is a continue signal (NXDOMAIN with Zero bit set)
|
||||||
|
if m.Rcode == dns.RcodeNameError && m.MsgHdr.Zero {
|
||||||
|
w.shouldContinue = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return w.ResponseWriter.WriteMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandlerChain() *HandlerChain {
|
||||||
|
return &HandlerChain{
|
||||||
|
handlers: make([]HandlerEntry, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrigPattern returns the original pattern of the handler that wrote the response
|
||||||
|
func (w *ResponseWriterChain) GetOrigPattern() string {
|
||||||
|
return w.origPattern
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHandler adds a new handler to the chain, replacing any existing handler with the same pattern and priority
|
||||||
|
func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority int, stopHandler handlerWithStop) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
origPattern := pattern
|
||||||
|
isWildcard := strings.HasPrefix(pattern, "*.")
|
||||||
|
if isWildcard {
|
||||||
|
pattern = pattern[2:]
|
||||||
|
}
|
||||||
|
pattern = dns.Fqdn(pattern)
|
||||||
|
origPattern = dns.Fqdn(origPattern)
|
||||||
|
|
||||||
|
// First remove any existing handler with same original pattern and priority
|
||||||
|
for i := len(c.handlers) - 1; i >= 0; i-- {
|
||||||
|
if c.handlers[i].OrigPattern == origPattern && c.handlers[i].Priority == priority {
|
||||||
|
if c.handlers[i].StopHandler != nil {
|
||||||
|
c.handlers[i].StopHandler.stop()
|
||||||
|
}
|
||||||
|
c.handlers = append(c.handlers[:i], c.handlers[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if handler implements SubdomainMatcher interface
|
||||||
|
matchSubdomains := false
|
||||||
|
if matcher, ok := handler.(SubdomainMatcher); ok {
|
||||||
|
matchSubdomains = matcher.MatchSubdomains()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("adding handler pattern: domain=%s original: domain=%s wildcard=%v match_subdomain=%v priority=%d",
|
||||||
|
pattern, origPattern, isWildcard, matchSubdomains, priority)
|
||||||
|
|
||||||
|
entry := HandlerEntry{
|
||||||
|
Handler: handler,
|
||||||
|
Priority: priority,
|
||||||
|
Pattern: pattern,
|
||||||
|
OrigPattern: origPattern,
|
||||||
|
IsWildcard: isWildcard,
|
||||||
|
StopHandler: stopHandler,
|
||||||
|
MatchSubdomains: matchSubdomains,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert handler in priority order
|
||||||
|
pos := 0
|
||||||
|
for i, h := range c.handlers {
|
||||||
|
if h.Priority < priority {
|
||||||
|
pos = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos = i + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
c.handlers = append(c.handlers[:pos], append([]HandlerEntry{entry}, c.handlers[pos:]...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveHandler removes a handler for the given pattern and priority
|
||||||
|
func (c *HandlerChain) RemoveHandler(pattern string, priority int) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
pattern = dns.Fqdn(pattern)
|
||||||
|
|
||||||
|
// Find and remove handlers matching both original pattern and priority
|
||||||
|
for i := len(c.handlers) - 1; i >= 0; i-- {
|
||||||
|
entry := c.handlers[i]
|
||||||
|
if entry.OrigPattern == pattern && entry.Priority == priority {
|
||||||
|
if entry.StopHandler != nil {
|
||||||
|
entry.StopHandler.stop()
|
||||||
|
}
|
||||||
|
c.handlers = append(c.handlers[:i], c.handlers[i+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasHandlers returns true if there are any handlers remaining for the given pattern
|
||||||
|
func (c *HandlerChain) HasHandlers(pattern string) bool {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
pattern = dns.Fqdn(pattern)
|
||||||
|
for _, entry := range c.handlers {
|
||||||
|
if entry.Pattern == pattern {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
if len(r.Question) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
qname := r.Question[0].Name
|
||||||
|
log.Tracef("handling DNS request for domain=%s", qname)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
log.Tracef("current handlers (%d):", len(c.handlers))
|
||||||
|
for _, h := range c.handlers {
|
||||||
|
log.Tracef(" - pattern: domain=%s original: domain=%s wildcard=%v priority=%d",
|
||||||
|
h.Pattern, h.OrigPattern, h.IsWildcard, h.Priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try handlers in priority order
|
||||||
|
for _, entry := range c.handlers {
|
||||||
|
var matched bool
|
||||||
|
switch {
|
||||||
|
case entry.Pattern == ".":
|
||||||
|
matched = true
|
||||||
|
case entry.IsWildcard:
|
||||||
|
parts := strings.Split(strings.TrimSuffix(qname, entry.Pattern), ".")
|
||||||
|
matched = len(parts) >= 2 && strings.HasSuffix(qname, entry.Pattern)
|
||||||
|
default:
|
||||||
|
// For non-wildcard patterns:
|
||||||
|
// If handler wants subdomain matching, allow suffix match
|
||||||
|
// Otherwise require exact match
|
||||||
|
if entry.MatchSubdomains {
|
||||||
|
matched = qname == entry.Pattern || strings.HasSuffix(qname, "."+entry.Pattern)
|
||||||
|
} else {
|
||||||
|
matched = qname == entry.Pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matched {
|
||||||
|
log.Tracef("trying domain match: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v matched=false",
|
||||||
|
qname, entry.OrigPattern, entry.MatchSubdomains, entry.IsWildcard)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("handler matched: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v",
|
||||||
|
qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains)
|
||||||
|
|
||||||
|
chainWriter := &ResponseWriterChain{
|
||||||
|
ResponseWriter: w,
|
||||||
|
origPattern: entry.OrigPattern,
|
||||||
|
}
|
||||||
|
entry.Handler.ServeDNS(chainWriter, r)
|
||||||
|
|
||||||
|
// If handler wants to continue, try next handler
|
||||||
|
if chainWriter.shouldContinue {
|
||||||
|
log.Tracef("handler requested continue to next handler")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No handler matched or all handlers passed
|
||||||
|
log.Tracef("no handler found for domain=%s", qname)
|
||||||
|
resp := &dns.Msg{}
|
||||||
|
resp.SetRcode(r, dns.RcodeNameError)
|
||||||
|
if err := w.WriteMsg(resp); err != nil {
|
||||||
|
log.Errorf("failed to write DNS response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
511
client/internal/dns/handler_chain_test.go
Normal file
511
client/internal/dns/handler_chain_test.go
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
package dns_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
nbdns "github.com/netbirdio/netbird/client/internal/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestHandlerChain_ServeDNS_Priorities tests that handlers are executed in priority order
|
||||||
|
func TestHandlerChain_ServeDNS_Priorities(t *testing.T) {
|
||||||
|
chain := nbdns.NewHandlerChain()
|
||||||
|
|
||||||
|
// Create mock handlers for different priorities
|
||||||
|
defaultHandler := &nbdns.MockHandler{}
|
||||||
|
matchDomainHandler := &nbdns.MockHandler{}
|
||||||
|
dnsRouteHandler := &nbdns.MockHandler{}
|
||||||
|
|
||||||
|
// Setup handlers with different priorities
|
||||||
|
chain.AddHandler("example.com.", defaultHandler, nbdns.PriorityDefault, nil)
|
||||||
|
chain.AddHandler("example.com.", matchDomainHandler, nbdns.PriorityMatchDomain, nil)
|
||||||
|
chain.AddHandler("example.com.", dnsRouteHandler, nbdns.PriorityDNSRoute, nil)
|
||||||
|
|
||||||
|
// Create test request
|
||||||
|
r := new(dns.Msg)
|
||||||
|
r.SetQuestion("example.com.", dns.TypeA)
|
||||||
|
|
||||||
|
// Create test writer
|
||||||
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
||||||
|
|
||||||
|
// Setup expectations - only highest priority handler should be called
|
||||||
|
dnsRouteHandler.On("ServeDNS", mock.Anything, r).Once()
|
||||||
|
matchDomainHandler.On("ServeDNS", mock.Anything, r).Maybe()
|
||||||
|
defaultHandler.On("ServeDNS", mock.Anything, r).Maybe()
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
chain.ServeDNS(w, r)
|
||||||
|
|
||||||
|
// Verify all expectations were met
|
||||||
|
dnsRouteHandler.AssertExpectations(t)
|
||||||
|
matchDomainHandler.AssertExpectations(t)
|
||||||
|
defaultHandler.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandlerChain_ServeDNS_DomainMatching tests various domain matching scenarios
|
||||||
|
func TestHandlerChain_ServeDNS_DomainMatching(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
handlerDomain string
|
||||||
|
queryDomain string
|
||||||
|
isWildcard bool
|
||||||
|
matchSubdomains bool
|
||||||
|
shouldMatch bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "exact match",
|
||||||
|
handlerDomain: "example.com.",
|
||||||
|
queryDomain: "example.com.",
|
||||||
|
isWildcard: false,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subdomain with non-wildcard and MatchSubdomains true",
|
||||||
|
handlerDomain: "example.com.",
|
||||||
|
queryDomain: "sub.example.com.",
|
||||||
|
isWildcard: false,
|
||||||
|
matchSubdomains: true,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subdomain with non-wildcard and MatchSubdomains false",
|
||||||
|
handlerDomain: "example.com.",
|
||||||
|
queryDomain: "sub.example.com.",
|
||||||
|
isWildcard: false,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard match",
|
||||||
|
handlerDomain: "*.example.com.",
|
||||||
|
queryDomain: "sub.example.com.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard no match on apex",
|
||||||
|
handlerDomain: "*.example.com.",
|
||||||
|
queryDomain: "example.com.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "root zone match",
|
||||||
|
handlerDomain: ".",
|
||||||
|
queryDomain: "anything.com.",
|
||||||
|
isWildcard: false,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no match different domain",
|
||||||
|
handlerDomain: "example.com.",
|
||||||
|
queryDomain: "example.org.",
|
||||||
|
isWildcard: false,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
chain := nbdns.NewHandlerChain()
|
||||||
|
var handler dns.Handler
|
||||||
|
|
||||||
|
if tt.matchSubdomains {
|
||||||
|
mockSubHandler := &nbdns.MockSubdomainHandler{Subdomains: true}
|
||||||
|
handler = mockSubHandler
|
||||||
|
if tt.shouldMatch {
|
||||||
|
mockSubHandler.On("ServeDNS", mock.Anything, mock.Anything).Once()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mockHandler := &nbdns.MockHandler{}
|
||||||
|
handler = mockHandler
|
||||||
|
if tt.shouldMatch {
|
||||||
|
mockHandler.On("ServeDNS", mock.Anything, mock.Anything).Once()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern := tt.handlerDomain
|
||||||
|
if tt.isWildcard {
|
||||||
|
pattern = "*." + tt.handlerDomain[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.AddHandler(pattern, handler, nbdns.PriorityDefault, nil)
|
||||||
|
|
||||||
|
r := new(dns.Msg)
|
||||||
|
r.SetQuestion(tt.queryDomain, dns.TypeA)
|
||||||
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
||||||
|
|
||||||
|
chain.ServeDNS(w, r)
|
||||||
|
|
||||||
|
if h, ok := handler.(*nbdns.MockHandler); ok {
|
||||||
|
h.AssertExpectations(t)
|
||||||
|
} else if h, ok := handler.(*nbdns.MockSubdomainHandler); ok {
|
||||||
|
h.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandlerChain_ServeDNS_OverlappingDomains tests behavior with overlapping domain patterns
|
||||||
|
func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
handlers []struct {
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
}
|
||||||
|
queryDomain string
|
||||||
|
expectedCalls int
|
||||||
|
expectedHandler int // index of the handler that should be called
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "wildcard and exact same priority - exact should win",
|
||||||
|
handlers: []struct {
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
}{
|
||||||
|
{pattern: "*.example.com.", priority: nbdns.PriorityDefault},
|
||||||
|
{pattern: "example.com.", priority: nbdns.PriorityDefault},
|
||||||
|
},
|
||||||
|
queryDomain: "example.com.",
|
||||||
|
expectedCalls: 1,
|
||||||
|
expectedHandler: 1, // exact match handler should be called
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "higher priority wildcard over lower priority exact",
|
||||||
|
handlers: []struct {
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
}{
|
||||||
|
{pattern: "example.com.", priority: nbdns.PriorityDefault},
|
||||||
|
{pattern: "*.example.com.", priority: nbdns.PriorityDNSRoute},
|
||||||
|
},
|
||||||
|
queryDomain: "test.example.com.",
|
||||||
|
expectedCalls: 1,
|
||||||
|
expectedHandler: 1, // higher priority wildcard handler should be called
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple wildcards different priorities",
|
||||||
|
handlers: []struct {
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
}{
|
||||||
|
{pattern: "*.example.com.", priority: nbdns.PriorityDefault},
|
||||||
|
{pattern: "*.example.com.", priority: nbdns.PriorityMatchDomain},
|
||||||
|
{pattern: "*.example.com.", priority: nbdns.PriorityDNSRoute},
|
||||||
|
},
|
||||||
|
queryDomain: "test.example.com.",
|
||||||
|
expectedCalls: 1,
|
||||||
|
expectedHandler: 2, // highest priority handler should be called
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subdomain with mix of patterns",
|
||||||
|
handlers: []struct {
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
}{
|
||||||
|
{pattern: "*.example.com.", priority: nbdns.PriorityDefault},
|
||||||
|
{pattern: "test.example.com.", priority: nbdns.PriorityMatchDomain},
|
||||||
|
{pattern: "*.test.example.com.", priority: nbdns.PriorityDNSRoute},
|
||||||
|
},
|
||||||
|
queryDomain: "sub.test.example.com.",
|
||||||
|
expectedCalls: 1,
|
||||||
|
expectedHandler: 2, // highest priority matching handler should be called
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "root zone with specific domain",
|
||||||
|
handlers: []struct {
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
}{
|
||||||
|
{pattern: ".", priority: nbdns.PriorityDefault},
|
||||||
|
{pattern: "example.com.", priority: nbdns.PriorityDNSRoute},
|
||||||
|
},
|
||||||
|
queryDomain: "example.com.",
|
||||||
|
expectedCalls: 1,
|
||||||
|
expectedHandler: 1, // higher priority specific domain should win over root
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
chain := nbdns.NewHandlerChain()
|
||||||
|
var handlers []*nbdns.MockHandler
|
||||||
|
|
||||||
|
// Setup handlers and expectations
|
||||||
|
for i := range tt.handlers {
|
||||||
|
handler := &nbdns.MockHandler{}
|
||||||
|
handlers = append(handlers, handler)
|
||||||
|
|
||||||
|
// Set expectation based on whether this handler should be called
|
||||||
|
if i == tt.expectedHandler {
|
||||||
|
handler.On("ServeDNS", mock.Anything, mock.Anything).Once()
|
||||||
|
} else {
|
||||||
|
handler.On("ServeDNS", mock.Anything, mock.Anything).Maybe()
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.AddHandler(tt.handlers[i].pattern, handler, tt.handlers[i].priority, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and execute request
|
||||||
|
r := new(dns.Msg)
|
||||||
|
r.SetQuestion(tt.queryDomain, dns.TypeA)
|
||||||
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
||||||
|
chain.ServeDNS(w, r)
|
||||||
|
|
||||||
|
// Verify expectations
|
||||||
|
for _, handler := range handlers {
|
||||||
|
handler.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandlerChain_ServeDNS_ChainContinuation tests the chain continuation functionality
|
||||||
|
func TestHandlerChain_ServeDNS_ChainContinuation(t *testing.T) {
|
||||||
|
chain := nbdns.NewHandlerChain()
|
||||||
|
|
||||||
|
// Create handlers
|
||||||
|
handler1 := &nbdns.MockHandler{}
|
||||||
|
handler2 := &nbdns.MockHandler{}
|
||||||
|
handler3 := &nbdns.MockHandler{}
|
||||||
|
|
||||||
|
// Add handlers in priority order
|
||||||
|
chain.AddHandler("example.com.", handler1, nbdns.PriorityDNSRoute, nil)
|
||||||
|
chain.AddHandler("example.com.", handler2, nbdns.PriorityMatchDomain, nil)
|
||||||
|
chain.AddHandler("example.com.", handler3, nbdns.PriorityDefault, nil)
|
||||||
|
|
||||||
|
// Create test request
|
||||||
|
r := new(dns.Msg)
|
||||||
|
r.SetQuestion("example.com.", dns.TypeA)
|
||||||
|
|
||||||
|
// Setup mock responses to simulate chain continuation
|
||||||
|
handler1.On("ServeDNS", mock.Anything, r).Run(func(args mock.Arguments) {
|
||||||
|
// First handler signals continue
|
||||||
|
w := args.Get(0).(*nbdns.ResponseWriterChain)
|
||||||
|
resp := new(dns.Msg)
|
||||||
|
resp.SetRcode(r, dns.RcodeNameError)
|
||||||
|
resp.MsgHdr.Zero = true // Signal to continue
|
||||||
|
assert.NoError(t, w.WriteMsg(resp))
|
||||||
|
}).Once()
|
||||||
|
|
||||||
|
handler2.On("ServeDNS", mock.Anything, r).Run(func(args mock.Arguments) {
|
||||||
|
// Second handler signals continue
|
||||||
|
w := args.Get(0).(*nbdns.ResponseWriterChain)
|
||||||
|
resp := new(dns.Msg)
|
||||||
|
resp.SetRcode(r, dns.RcodeNameError)
|
||||||
|
resp.MsgHdr.Zero = true
|
||||||
|
assert.NoError(t, w.WriteMsg(resp))
|
||||||
|
}).Once()
|
||||||
|
|
||||||
|
handler3.On("ServeDNS", mock.Anything, r).Run(func(args mock.Arguments) {
|
||||||
|
// Last handler responds normally
|
||||||
|
w := args.Get(0).(*nbdns.ResponseWriterChain)
|
||||||
|
resp := new(dns.Msg)
|
||||||
|
resp.SetRcode(r, dns.RcodeSuccess)
|
||||||
|
assert.NoError(t, w.WriteMsg(resp))
|
||||||
|
}).Once()
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
||||||
|
chain.ServeDNS(w, r)
|
||||||
|
|
||||||
|
// Verify all handlers were called in order
|
||||||
|
handler1.AssertExpectations(t)
|
||||||
|
handler2.AssertExpectations(t)
|
||||||
|
handler3.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockResponseWriter implements dns.ResponseWriter for testing
|
||||||
|
type mockResponseWriter struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockResponseWriter) LocalAddr() net.Addr { return nil }
|
||||||
|
func (m *mockResponseWriter) RemoteAddr() net.Addr { return nil }
|
||||||
|
func (m *mockResponseWriter) WriteMsg(*dns.Msg) error { return nil }
|
||||||
|
func (m *mockResponseWriter) Write([]byte) (int, error) { return 0, nil }
|
||||||
|
func (m *mockResponseWriter) Close() error { return nil }
|
||||||
|
func (m *mockResponseWriter) TsigStatus() error { return nil }
|
||||||
|
func (m *mockResponseWriter) TsigTimersOnly(bool) {}
|
||||||
|
func (m *mockResponseWriter) Hijack() {}
|
||||||
|
|
||||||
|
func TestHandlerChain_PriorityDeregistration(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ops []struct {
|
||||||
|
action string // "add" or "remove"
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
}
|
||||||
|
query string
|
||||||
|
expectedCalls map[int]bool // map[priority]shouldBeCalled
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "remove high priority keeps lower priority handler",
|
||||||
|
ops: []struct {
|
||||||
|
action string
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
}{
|
||||||
|
{"add", "example.com.", nbdns.PriorityDNSRoute},
|
||||||
|
{"add", "example.com.", nbdns.PriorityMatchDomain},
|
||||||
|
{"remove", "example.com.", nbdns.PriorityDNSRoute},
|
||||||
|
},
|
||||||
|
query: "example.com.",
|
||||||
|
expectedCalls: map[int]bool{
|
||||||
|
nbdns.PriorityDNSRoute: false,
|
||||||
|
nbdns.PriorityMatchDomain: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove lower priority keeps high priority handler",
|
||||||
|
ops: []struct {
|
||||||
|
action string
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
}{
|
||||||
|
{"add", "example.com.", nbdns.PriorityDNSRoute},
|
||||||
|
{"add", "example.com.", nbdns.PriorityMatchDomain},
|
||||||
|
{"remove", "example.com.", nbdns.PriorityMatchDomain},
|
||||||
|
},
|
||||||
|
query: "example.com.",
|
||||||
|
expectedCalls: map[int]bool{
|
||||||
|
nbdns.PriorityDNSRoute: true,
|
||||||
|
nbdns.PriorityMatchDomain: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove all handlers in order",
|
||||||
|
ops: []struct {
|
||||||
|
action string
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
}{
|
||||||
|
{"add", "example.com.", nbdns.PriorityDNSRoute},
|
||||||
|
{"add", "example.com.", nbdns.PriorityMatchDomain},
|
||||||
|
{"add", "example.com.", nbdns.PriorityDefault},
|
||||||
|
{"remove", "example.com.", nbdns.PriorityDNSRoute},
|
||||||
|
{"remove", "example.com.", nbdns.PriorityMatchDomain},
|
||||||
|
},
|
||||||
|
query: "example.com.",
|
||||||
|
expectedCalls: map[int]bool{
|
||||||
|
nbdns.PriorityDNSRoute: false,
|
||||||
|
nbdns.PriorityMatchDomain: false,
|
||||||
|
nbdns.PriorityDefault: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
chain := nbdns.NewHandlerChain()
|
||||||
|
handlers := make(map[int]*nbdns.MockHandler)
|
||||||
|
|
||||||
|
// Execute operations
|
||||||
|
for _, op := range tt.ops {
|
||||||
|
if op.action == "add" {
|
||||||
|
handler := &nbdns.MockHandler{}
|
||||||
|
handlers[op.priority] = handler
|
||||||
|
chain.AddHandler(op.pattern, handler, op.priority, nil)
|
||||||
|
} else {
|
||||||
|
chain.RemoveHandler(op.pattern, op.priority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test request
|
||||||
|
r := new(dns.Msg)
|
||||||
|
r.SetQuestion(tt.query, dns.TypeA)
|
||||||
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
||||||
|
|
||||||
|
// Setup expectations
|
||||||
|
for priority, handler := range handlers {
|
||||||
|
if shouldCall, exists := tt.expectedCalls[priority]; exists && shouldCall {
|
||||||
|
handler.On("ServeDNS", mock.Anything, r).Once()
|
||||||
|
} else {
|
||||||
|
handler.On("ServeDNS", mock.Anything, r).Maybe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute request
|
||||||
|
chain.ServeDNS(w, r)
|
||||||
|
|
||||||
|
// Verify expectations
|
||||||
|
for _, handler := range handlers {
|
||||||
|
handler.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify handler exists check
|
||||||
|
for priority, shouldExist := range tt.expectedCalls {
|
||||||
|
if shouldExist {
|
||||||
|
assert.True(t, chain.HasHandlers(tt.ops[0].pattern),
|
||||||
|
"Handler chain should have handlers for pattern after removing priority %d", priority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
|
||||||
|
chain := nbdns.NewHandlerChain()
|
||||||
|
|
||||||
|
testDomain := "example.com."
|
||||||
|
testQuery := "test.example.com."
|
||||||
|
|
||||||
|
// Create handlers with MatchSubdomains enabled
|
||||||
|
routeHandler := &nbdns.MockSubdomainHandler{Subdomains: true}
|
||||||
|
matchHandler := &nbdns.MockSubdomainHandler{Subdomains: true}
|
||||||
|
defaultHandler := &nbdns.MockSubdomainHandler{Subdomains: true}
|
||||||
|
|
||||||
|
// Create test request that will be reused
|
||||||
|
r := new(dns.Msg)
|
||||||
|
r.SetQuestion(testQuery, dns.TypeA)
|
||||||
|
|
||||||
|
// Add handlers in mixed order
|
||||||
|
chain.AddHandler(testDomain, defaultHandler, nbdns.PriorityDefault, nil)
|
||||||
|
chain.AddHandler(testDomain, routeHandler, nbdns.PriorityDNSRoute, nil)
|
||||||
|
chain.AddHandler(testDomain, matchHandler, nbdns.PriorityMatchDomain, nil)
|
||||||
|
|
||||||
|
// Test 1: Initial state with all three handlers
|
||||||
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
||||||
|
// Highest priority handler (routeHandler) should be called
|
||||||
|
routeHandler.On("ServeDNS", mock.Anything, r).Return().Once()
|
||||||
|
|
||||||
|
chain.ServeDNS(w, r)
|
||||||
|
routeHandler.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Test 2: Remove highest priority handler
|
||||||
|
chain.RemoveHandler(testDomain, nbdns.PriorityDNSRoute)
|
||||||
|
assert.True(t, chain.HasHandlers(testDomain))
|
||||||
|
|
||||||
|
w = &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
||||||
|
// Now middle priority handler (matchHandler) should be called
|
||||||
|
matchHandler.On("ServeDNS", mock.Anything, r).Return().Once()
|
||||||
|
|
||||||
|
chain.ServeDNS(w, r)
|
||||||
|
matchHandler.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Test 3: Remove middle priority handler
|
||||||
|
chain.RemoveHandler(testDomain, nbdns.PriorityMatchDomain)
|
||||||
|
assert.True(t, chain.HasHandlers(testDomain))
|
||||||
|
|
||||||
|
w = &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
||||||
|
// Now lowest priority handler (defaultHandler) should be called
|
||||||
|
defaultHandler.On("ServeDNS", mock.Anything, r).Return().Once()
|
||||||
|
|
||||||
|
chain.ServeDNS(w, r)
|
||||||
|
defaultHandler.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Test 4: Remove last handler
|
||||||
|
chain.RemoveHandler(testDomain, nbdns.PriorityDefault)
|
||||||
|
assert.False(t, chain.HasHandlers(testDomain))
|
||||||
|
}
|
||||||
@@ -17,12 +17,24 @@ type localResolver struct {
|
|||||||
records sync.Map
|
records sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *localResolver) MatchSubdomains() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (d *localResolver) stop() {
|
func (d *localResolver) stop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the local resolver
|
||||||
|
func (d *localResolver) String() string {
|
||||||
|
return fmt.Sprintf("local resolver [%d records]", len(d.registeredMap))
|
||||||
|
}
|
||||||
|
|
||||||
// ServeDNS handles a DNS request
|
// ServeDNS handles a DNS request
|
||||||
func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
log.Tracef("received question: %#v", r.Question[0])
|
if len(r.Question) > 0 {
|
||||||
|
log.Tracef("received question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass)
|
||||||
|
}
|
||||||
|
|
||||||
replyMessage := &dns.Msg{}
|
replyMessage := &dns.Msg{}
|
||||||
replyMessage.SetReply(r)
|
replyMessage.SetReply(r)
|
||||||
replyMessage.RecursionAvailable = true
|
replyMessage.RecursionAvailable = true
|
||||||
|
|||||||
@@ -3,14 +3,30 @@ package dns
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockServer is the mock instance of a dns server
|
// MockServer is the mock instance of a dns server
|
||||||
type MockServer struct {
|
type MockServer struct {
|
||||||
InitializeFunc func() error
|
InitializeFunc func() error
|
||||||
StopFunc func()
|
StopFunc func()
|
||||||
UpdateDNSServerFunc func(serial uint64, update nbdns.Config) error
|
UpdateDNSServerFunc func(serial uint64, update nbdns.Config) error
|
||||||
|
RegisterHandlerFunc func([]string, dns.Handler, int)
|
||||||
|
DeregisterHandlerFunc func([]string, int)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) RegisterHandler(domains []string, handler dns.Handler, priority int) {
|
||||||
|
if m.RegisterHandlerFunc != nil {
|
||||||
|
m.RegisterHandlerFunc(domains, handler, priority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) DeregisterHandler(domains []string, priority int) {
|
||||||
|
if m.DeregisterHandlerFunc != nil {
|
||||||
|
m.DeregisterHandlerFunc(domains, priority)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize mock implementation of Initialize from Server interface
|
// Initialize mock implementation of Initialize from Server interface
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/mitchellh/hashstructure/v2"
|
"github.com/mitchellh/hashstructure/v2"
|
||||||
@@ -31,6 +30,8 @@ type IosDnsManager interface {
|
|||||||
|
|
||||||
// Server is a dns server interface
|
// Server is a dns server interface
|
||||||
type Server interface {
|
type Server interface {
|
||||||
|
RegisterHandler(domains []string, handler dns.Handler, priority int)
|
||||||
|
DeregisterHandler(domains []string, priority int)
|
||||||
Initialize() error
|
Initialize() error
|
||||||
Stop()
|
Stop()
|
||||||
DnsIP() string
|
DnsIP() string
|
||||||
@@ -49,12 +50,14 @@ type DefaultServer struct {
|
|||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
service service
|
service service
|
||||||
dnsMuxMap registeredHandlerMap
|
dnsMuxMap registeredHandlerMap
|
||||||
|
handlerPriorities map[string]int
|
||||||
localResolver *localResolver
|
localResolver *localResolver
|
||||||
wgInterface WGIface
|
wgInterface WGIface
|
||||||
hostManager hostManager
|
hostManager hostManager
|
||||||
updateSerial uint64
|
updateSerial uint64
|
||||||
previousConfigHash uint64
|
previousConfigHash uint64
|
||||||
currentConfig HostDNSConfig
|
currentConfig HostDNSConfig
|
||||||
|
handlerChain *HandlerChain
|
||||||
|
|
||||||
// permanent related properties
|
// permanent related properties
|
||||||
permanent bool
|
permanent bool
|
||||||
@@ -75,8 +78,9 @@ type handlerWithStop interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type muxUpdate struct {
|
type muxUpdate struct {
|
||||||
domain string
|
domain string
|
||||||
handler handlerWithStop
|
handler handlerWithStop
|
||||||
|
priority int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDefaultServer returns a new dns server
|
// NewDefaultServer returns a new dns server
|
||||||
@@ -136,10 +140,12 @@ func NewDefaultServerIos(
|
|||||||
func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService service, statusRecorder *peer.Status, stateManager *statemanager.Manager) *DefaultServer {
|
func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService service, statusRecorder *peer.Status, stateManager *statemanager.Manager) *DefaultServer {
|
||||||
ctx, stop := context.WithCancel(ctx)
|
ctx, stop := context.WithCancel(ctx)
|
||||||
defaultServer := &DefaultServer{
|
defaultServer := &DefaultServer{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
ctxCancel: stop,
|
ctxCancel: stop,
|
||||||
service: dnsService,
|
service: dnsService,
|
||||||
dnsMuxMap: make(registeredHandlerMap),
|
handlerChain: NewHandlerChain(),
|
||||||
|
dnsMuxMap: make(registeredHandlerMap),
|
||||||
|
handlerPriorities: make(map[string]int),
|
||||||
localResolver: &localResolver{
|
localResolver: &localResolver{
|
||||||
registeredMap: make(registrationMap),
|
registeredMap: make(registrationMap),
|
||||||
},
|
},
|
||||||
@@ -152,6 +158,41 @@ func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService servi
|
|||||||
return defaultServer
|
return defaultServer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) RegisterHandler(domains []string, handler dns.Handler, priority int) {
|
||||||
|
s.mux.Lock()
|
||||||
|
defer s.mux.Unlock()
|
||||||
|
|
||||||
|
s.registerHandler(domains, handler, priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, priority int) {
|
||||||
|
log.Debugf("registering handler %s with priority %d", handler, priority)
|
||||||
|
|
||||||
|
for _, domain := range domains {
|
||||||
|
s.handlerChain.AddHandler(domain, handler, priority, nil)
|
||||||
|
s.handlerPriorities[domain] = priority
|
||||||
|
s.service.RegisterMux(nbdns.NormalizeZone(domain), s.handlerChain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) DeregisterHandler(domains []string, priority int) {
|
||||||
|
s.mux.Lock()
|
||||||
|
defer s.mux.Unlock()
|
||||||
|
|
||||||
|
s.deregisterHandler(domains, priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) deregisterHandler(domains []string, priority int) {
|
||||||
|
for _, domain := range domains {
|
||||||
|
s.handlerChain.RemoveHandler(domain, priority)
|
||||||
|
|
||||||
|
// Only deregister from service if no handlers remain
|
||||||
|
if !s.handlerChain.HasHandlers(domain) {
|
||||||
|
s.service.DeregisterMux(nbdns.NormalizeZone(domain))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize instantiate host manager and the dns service
|
// Initialize instantiate host manager and the dns service
|
||||||
func (s *DefaultServer) Initialize() (err error) {
|
func (s *DefaultServer) Initialize() (err error) {
|
||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
@@ -323,12 +364,12 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
|
|||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// persist dns state right away
|
go func() {
|
||||||
ctx, cancel := context.WithTimeout(s.ctx, 3*time.Second)
|
// persist dns state right away
|
||||||
defer cancel()
|
if err := s.stateManager.PersistState(s.ctx); err != nil {
|
||||||
if err := s.stateManager.PersistState(ctx); err != nil {
|
log.Errorf("Failed to persist dns state: %v", err)
|
||||||
log.Errorf("Failed to persist dns state: %v", err)
|
}
|
||||||
}
|
}()
|
||||||
|
|
||||||
if s.searchDomainNotifier != nil {
|
if s.searchDomainNotifier != nil {
|
||||||
s.searchDomainNotifier.onNewSearchDomains(s.SearchDomains())
|
s.searchDomainNotifier.onNewSearchDomains(s.SearchDomains())
|
||||||
@@ -344,14 +385,14 @@ func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone)
|
|||||||
localRecords := make(map[string]nbdns.SimpleRecord, 0)
|
localRecords := make(map[string]nbdns.SimpleRecord, 0)
|
||||||
|
|
||||||
for _, customZone := range customZones {
|
for _, customZone := range customZones {
|
||||||
|
|
||||||
if len(customZone.Records) == 0 {
|
if len(customZone.Records) == 0 {
|
||||||
return nil, nil, fmt.Errorf("received an empty list of records")
|
return nil, nil, fmt.Errorf("received an empty list of records")
|
||||||
}
|
}
|
||||||
|
|
||||||
muxUpdates = append(muxUpdates, muxUpdate{
|
muxUpdates = append(muxUpdates, muxUpdate{
|
||||||
domain: customZone.Domain,
|
domain: customZone.Domain,
|
||||||
handler: s.localResolver,
|
handler: s.localResolver,
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, record := range customZone.Records {
|
for _, record := range customZone.Records {
|
||||||
@@ -413,8 +454,9 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam
|
|||||||
|
|
||||||
if nsGroup.Primary {
|
if nsGroup.Primary {
|
||||||
muxUpdates = append(muxUpdates, muxUpdate{
|
muxUpdates = append(muxUpdates, muxUpdate{
|
||||||
domain: nbdns.RootZone,
|
domain: nbdns.RootZone,
|
||||||
handler: handler,
|
handler: handler,
|
||||||
|
priority: PriorityDefault,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -430,8 +472,9 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam
|
|||||||
return nil, fmt.Errorf("received a nameserver group with an empty domain element")
|
return nil, fmt.Errorf("received a nameserver group with an empty domain element")
|
||||||
}
|
}
|
||||||
muxUpdates = append(muxUpdates, muxUpdate{
|
muxUpdates = append(muxUpdates, muxUpdate{
|
||||||
domain: domain,
|
domain: domain,
|
||||||
handler: handler,
|
handler: handler,
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,12 +484,16 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam
|
|||||||
|
|
||||||
func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) {
|
func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) {
|
||||||
muxUpdateMap := make(registeredHandlerMap)
|
muxUpdateMap := make(registeredHandlerMap)
|
||||||
|
handlersByPriority := make(map[string]int)
|
||||||
|
|
||||||
var isContainRootUpdate bool
|
var isContainRootUpdate bool
|
||||||
|
|
||||||
|
// First register new handlers
|
||||||
for _, update := range muxUpdates {
|
for _, update := range muxUpdates {
|
||||||
s.service.RegisterMux(update.domain, update.handler)
|
s.registerHandler([]string{update.domain}, update.handler, update.priority)
|
||||||
muxUpdateMap[update.domain] = update.handler
|
muxUpdateMap[update.domain] = update.handler
|
||||||
|
handlersByPriority[update.domain] = update.priority
|
||||||
|
|
||||||
if existingHandler, ok := s.dnsMuxMap[update.domain]; ok {
|
if existingHandler, ok := s.dnsMuxMap[update.domain]; ok {
|
||||||
existingHandler.stop()
|
existingHandler.stop()
|
||||||
}
|
}
|
||||||
@@ -456,6 +503,7 @@ func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Then deregister old handlers not in the update
|
||||||
for key, existingHandler := range s.dnsMuxMap {
|
for key, existingHandler := range s.dnsMuxMap {
|
||||||
_, found := muxUpdateMap[key]
|
_, found := muxUpdateMap[key]
|
||||||
if !found {
|
if !found {
|
||||||
@@ -464,12 +512,16 @@ func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) {
|
|||||||
existingHandler.stop()
|
existingHandler.stop()
|
||||||
} else {
|
} else {
|
||||||
existingHandler.stop()
|
existingHandler.stop()
|
||||||
s.service.DeregisterMux(key)
|
// Deregister with the priority that was used to register
|
||||||
|
if oldPriority, ok := s.handlerPriorities[key]; ok {
|
||||||
|
s.deregisterHandler([]string{key}, oldPriority)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.dnsMuxMap = muxUpdateMap
|
s.dnsMuxMap = muxUpdateMap
|
||||||
|
s.handlerPriorities = handlersByPriority
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) {
|
func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) {
|
||||||
@@ -518,13 +570,13 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
if nsGroup.Primary {
|
if nsGroup.Primary {
|
||||||
removeIndex[nbdns.RootZone] = -1
|
removeIndex[nbdns.RootZone] = -1
|
||||||
s.currentConfig.RouteAll = false
|
s.currentConfig.RouteAll = false
|
||||||
s.service.DeregisterMux(nbdns.RootZone)
|
s.deregisterHandler([]string{nbdns.RootZone}, PriorityDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, item := range s.currentConfig.Domains {
|
for i, item := range s.currentConfig.Domains {
|
||||||
if _, found := removeIndex[item.Domain]; found {
|
if _, found := removeIndex[item.Domain]; found {
|
||||||
s.currentConfig.Domains[i].Disabled = true
|
s.currentConfig.Domains[i].Disabled = true
|
||||||
s.service.DeregisterMux(item.Domain)
|
s.deregisterHandler([]string{item.Domain}, PriorityMatchDomain)
|
||||||
removeIndex[item.Domain] = i
|
removeIndex[item.Domain] = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,12 +585,11 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
l.Errorf("Failed to apply nameserver deactivation on the host: %v", err)
|
l.Errorf("Failed to apply nameserver deactivation on the host: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// persist dns state right away
|
go func() {
|
||||||
ctx, cancel := context.WithTimeout(s.ctx, 3*time.Second)
|
if err := s.stateManager.PersistState(s.ctx); err != nil {
|
||||||
defer cancel()
|
l.Errorf("Failed to persist dns state: %v", err)
|
||||||
if err := s.stateManager.PersistState(ctx); err != nil {
|
}
|
||||||
l.Errorf("Failed to persist dns state: %v", err)
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOOS == "android" && nsGroup.Primary && len(s.hostsDNSHolder.get()) > 0 {
|
if runtime.GOOS == "android" && nsGroup.Primary && len(s.hostsDNSHolder.get()) > 0 {
|
||||||
s.addHostRootZone()
|
s.addHostRootZone()
|
||||||
@@ -556,7 +607,7 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.currentConfig.Domains[i].Disabled = false
|
s.currentConfig.Domains[i].Disabled = false
|
||||||
s.service.RegisterMux(domain, handler)
|
s.registerHandler([]string{domain}, handler, PriorityMatchDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
l := log.WithField("nameservers", nsGroup.NameServers)
|
l := log.WithField("nameservers", nsGroup.NameServers)
|
||||||
@@ -564,7 +615,7 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
|
|
||||||
if nsGroup.Primary {
|
if nsGroup.Primary {
|
||||||
s.currentConfig.RouteAll = true
|
s.currentConfig.RouteAll = true
|
||||||
s.service.RegisterMux(nbdns.RootZone, handler)
|
s.registerHandler([]string{nbdns.RootZone}, handler, PriorityDefault)
|
||||||
}
|
}
|
||||||
if err := s.hostManager.applyDNSConfig(s.currentConfig, s.stateManager); err != nil {
|
if err := s.hostManager.applyDNSConfig(s.currentConfig, s.stateManager); err != nil {
|
||||||
l.WithError(err).Error("reactivate temporary disabled nameserver group, DNS update apply")
|
l.WithError(err).Error("reactivate temporary disabled nameserver group, DNS update apply")
|
||||||
@@ -595,7 +646,8 @@ func (s *DefaultServer) addHostRootZone() {
|
|||||||
}
|
}
|
||||||
handler.deactivate = func(error) {}
|
handler.deactivate = func(error) {}
|
||||||
handler.reactivate = func() {}
|
handler.reactivate = func() {}
|
||||||
s.service.RegisterMux(nbdns.RootZone, handler)
|
|
||||||
|
s.registerHandler([]string{nbdns.RootZone}, handler, PriorityDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) updateNSGroupStates(groups []*nbdns.NameServerGroup) {
|
func (s *DefaultServer) updateNSGroupStates(groups []*nbdns.NameServerGroup) {
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/miekg/dns"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter"
|
"github.com/netbirdio/netbird/client/firewall/uspfilter"
|
||||||
@@ -512,7 +514,7 @@ func TestDNSServerStartStop(t *testing.T) {
|
|||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsServer.service.RegisterMux("netbird.cloud", dnsServer.localResolver)
|
dnsServer.registerHandler([]string{"netbird.cloud"}, dnsServer.localResolver, 1)
|
||||||
|
|
||||||
resolver := &net.Resolver{
|
resolver := &net.Resolver{
|
||||||
PreferGo: true,
|
PreferGo: true,
|
||||||
@@ -560,7 +562,9 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
|
|||||||
localResolver: &localResolver{
|
localResolver: &localResolver{
|
||||||
registeredMap: make(registrationMap),
|
registeredMap: make(registrationMap),
|
||||||
},
|
},
|
||||||
hostManager: hostManager,
|
handlerChain: NewHandlerChain(),
|
||||||
|
handlerPriorities: make(map[string]int),
|
||||||
|
hostManager: hostManager,
|
||||||
currentConfig: HostDNSConfig{
|
currentConfig: HostDNSConfig{
|
||||||
Domains: []DomainConfig{
|
Domains: []DomainConfig{
|
||||||
{false, "domain0", false},
|
{false, "domain0", false},
|
||||||
@@ -782,7 +786,7 @@ func TestDNSPermanent_matchOnly(t *testing.T) {
|
|||||||
Port: 53,
|
Port: 53,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Domains: []string{"customdomain.com"},
|
Domains: []string{"google.com"},
|
||||||
Primary: false,
|
Primary: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -804,7 +808,7 @@ func TestDNSPermanent_matchOnly(t *testing.T) {
|
|||||||
if ips[0] != zoneRecords[0].RData {
|
if ips[0] != zoneRecords[0].RData {
|
||||||
t.Fatalf("invalid zone record: %v", err)
|
t.Fatalf("invalid zone record: %v", err)
|
||||||
}
|
}
|
||||||
_, err = resolver.LookupHost(context.Background(), "customdomain.com")
|
_, err = resolver.LookupHost(context.Background(), "google.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to resolve: %s", err)
|
t.Errorf("failed to resolve: %s", err)
|
||||||
}
|
}
|
||||||
@@ -872,3 +876,86 @@ func newDnsResolver(ip string, port int) *net.Resolver {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MockHandler implements dns.Handler interface for testing
|
||||||
|
type MockHandler struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
m.Called(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockSubdomainHandler struct {
|
||||||
|
MockHandler
|
||||||
|
Subdomains bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSubdomainHandler) MatchSubdomains() bool {
|
||||||
|
return m.Subdomains
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandlerChain_DomainPriorities(t *testing.T) {
|
||||||
|
chain := NewHandlerChain()
|
||||||
|
|
||||||
|
dnsRouteHandler := &MockHandler{}
|
||||||
|
upstreamHandler := &MockSubdomainHandler{
|
||||||
|
Subdomains: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.AddHandler("example.com.", dnsRouteHandler, PriorityDNSRoute, nil)
|
||||||
|
chain.AddHandler("example.com.", upstreamHandler, PriorityMatchDomain, nil)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
query string
|
||||||
|
expectedHandler dns.Handler
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "exact domain with dns route handler",
|
||||||
|
query: "example.com.",
|
||||||
|
expectedHandler: dnsRouteHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subdomain should use upstream handler",
|
||||||
|
query: "sub.example.com.",
|
||||||
|
expectedHandler: upstreamHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deep subdomain should use upstream handler",
|
||||||
|
query: "deep.sub.example.com.",
|
||||||
|
expectedHandler: upstreamHandler,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
r := new(dns.Msg)
|
||||||
|
r.SetQuestion(tc.query, dns.TypeA)
|
||||||
|
w := &ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
||||||
|
|
||||||
|
if mh, ok := tc.expectedHandler.(*MockHandler); ok {
|
||||||
|
mh.On("ServeDNS", mock.Anything, r).Once()
|
||||||
|
} else if mh, ok := tc.expectedHandler.(*MockSubdomainHandler); ok {
|
||||||
|
mh.On("ServeDNS", mock.Anything, r).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.ServeDNS(w, r)
|
||||||
|
|
||||||
|
if mh, ok := tc.expectedHandler.(*MockHandler); ok {
|
||||||
|
mh.AssertExpectations(t)
|
||||||
|
} else if mh, ok := tc.expectedHandler.(*MockSubdomainHandler); ok {
|
||||||
|
mh.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset mocks
|
||||||
|
if mh, ok := tc.expectedHandler.(*MockHandler); ok {
|
||||||
|
mh.ExpectedCalls = nil
|
||||||
|
mh.Calls = nil
|
||||||
|
} else if mh, ok := tc.expectedHandler.(*MockSubdomainHandler); ok {
|
||||||
|
mh.ExpectedCalls = nil
|
||||||
|
mh.Calls = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ func (s *serviceViaListener) Stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceViaListener) RegisterMux(pattern string, handler dns.Handler) {
|
func (s *serviceViaListener) RegisterMux(pattern string, handler dns.Handler) {
|
||||||
|
log.Debugf("registering dns handler for pattern: %s", pattern)
|
||||||
s.dnsMux.Handle(pattern, handler)
|
s.dnsMux.Handle(pattern, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,15 @@ func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status) *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the upstream resolver
|
||||||
|
func (u *upstreamResolverBase) String() string {
|
||||||
|
return fmt.Sprintf("upstream %v", u.upstreamServers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolverBase) MatchSubdomains() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverBase) stop() {
|
func (u *upstreamResolverBase) stop() {
|
||||||
log.Debugf("stopping serving DNS for upstreams %s", u.upstreamServers)
|
log.Debugf("stopping serving DNS for upstreams %s", u.upstreamServers)
|
||||||
u.cancel()
|
u.cancel()
|
||||||
|
|||||||
143
client/internal/dnsfwd/forwarder.go
Normal file
143
client/internal/dnsfwd/forwarder.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package dnsfwd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
const errResolveFailed = "failed to resolve query for domain=%s: %v"
|
||||||
|
|
||||||
|
type DNSForwarder struct {
|
||||||
|
listenAddress string
|
||||||
|
ttl uint32
|
||||||
|
domains []string
|
||||||
|
|
||||||
|
dnsServer *dns.Server
|
||||||
|
mux *dns.ServeMux
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDNSForwarder(listenAddress string, ttl uint32, domains []string) *DNSForwarder {
|
||||||
|
log.Debugf("creating DNS forwarder with listen_address=%s ttl=%d domains=%v", listenAddress, ttl, domains)
|
||||||
|
return &DNSForwarder{
|
||||||
|
listenAddress: listenAddress,
|
||||||
|
ttl: ttl,
|
||||||
|
domains: domains,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DNSForwarder) Listen() error {
|
||||||
|
log.Infof("listen DNS forwarder on address=%s", f.listenAddress)
|
||||||
|
mux := dns.NewServeMux()
|
||||||
|
|
||||||
|
for _, d := range f.domains {
|
||||||
|
mux.HandleFunc(nbdns.NormalizeZone(d), f.handleDNSQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsServer := &dns.Server{
|
||||||
|
Addr: f.listenAddress,
|
||||||
|
Net: "udp",
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
f.dnsServer = dnsServer
|
||||||
|
f.mux = mux
|
||||||
|
return dnsServer.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DNSForwarder) UpdateDomains(domains []string) {
|
||||||
|
for _, d := range f.domains {
|
||||||
|
f.mux.HandleRemove(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range f.domains {
|
||||||
|
f.mux.HandleFunc(nbdns.NormalizeZone(d), f.handleDNSQuery)
|
||||||
|
}
|
||||||
|
f.domains = domains
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DNSForwarder) Close(ctx context.Context) error {
|
||||||
|
if f.dnsServer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return f.dnsServer.ShutdownContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DNSForwarder) handleDNSQuery(w dns.ResponseWriter, query *dns.Msg) {
|
||||||
|
if len(query.Question) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Tracef("received DNS request for DNS forwarder: domain=%v type=%v class=%v",
|
||||||
|
query.Question[0].Name, query.Question[0].Qtype, query.Question[0].Qclass)
|
||||||
|
|
||||||
|
question := query.Question[0]
|
||||||
|
domain := question.Name
|
||||||
|
|
||||||
|
resp := query.SetReply(query)
|
||||||
|
|
||||||
|
ips, err := net.LookupIP(domain)
|
||||||
|
if err != nil {
|
||||||
|
var dnsErr *net.DNSError
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &dnsErr):
|
||||||
|
resp.Rcode = dns.RcodeServerFailure
|
||||||
|
if dnsErr.IsNotFound {
|
||||||
|
// Pass through NXDOMAIN
|
||||||
|
resp.Rcode = dns.RcodeNameError
|
||||||
|
}
|
||||||
|
|
||||||
|
if dnsErr.Server != "" {
|
||||||
|
log.Warnf("failed to resolve query for domain=%s server=%s: %v", domain, dnsErr.Server, err)
|
||||||
|
} else {
|
||||||
|
log.Warnf(errResolveFailed, domain, err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
resp.Rcode = dns.RcodeServerFailure
|
||||||
|
log.Warnf(errResolveFailed, domain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.WriteMsg(resp); err != nil {
|
||||||
|
log.Errorf("failed to write failure DNS response: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
var respRecord dns.RR
|
||||||
|
if ip.To4() == nil {
|
||||||
|
log.Tracef("resolved domain=%s to IPv6=%s", domain, ip)
|
||||||
|
rr := dns.AAAA{
|
||||||
|
AAAA: ip,
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: domain,
|
||||||
|
Rrtype: dns.TypeAAAA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: f.ttl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
respRecord = &rr
|
||||||
|
} else {
|
||||||
|
log.Tracef("resolved domain=%s to IPv4=%s", domain, ip)
|
||||||
|
rr := dns.A{
|
||||||
|
A: ip,
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: domain,
|
||||||
|
Rrtype: dns.TypeA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: f.ttl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
respRecord = &rr
|
||||||
|
}
|
||||||
|
resp.Answer = append(resp.Answer, respRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.WriteMsg(resp); err != nil {
|
||||||
|
log.Errorf("failed to write DNS response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
106
client/internal/dnsfwd/manager.go
Normal file
106
client/internal/dnsfwd/manager.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package dnsfwd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ListenPort is the port that the DNS forwarder listens on. It has been used by the client peers also
|
||||||
|
ListenPort = 5353
|
||||||
|
dnsTTL = 60 //seconds
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
firewall firewall.Manager
|
||||||
|
|
||||||
|
fwRules []firewall.Rule
|
||||||
|
dnsForwarder *DNSForwarder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(fw firewall.Manager) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
firewall: fw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Start(domains []string) error {
|
||||||
|
log.Infof("starting DNS forwarder")
|
||||||
|
if m.dnsForwarder != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.allowDNSFirewall(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.dnsForwarder = NewDNSForwarder(fmt.Sprintf(":%d", ListenPort), dnsTTL, domains)
|
||||||
|
go func() {
|
||||||
|
if err := m.dnsForwarder.Listen(); err != nil {
|
||||||
|
// todo handle close error if it is exists
|
||||||
|
log.Errorf("failed to start DNS forwarder, err: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) UpdateDomains(domains []string) {
|
||||||
|
if m.dnsForwarder == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.dnsForwarder.UpdateDomains(domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Stop(ctx context.Context) error {
|
||||||
|
if m.dnsForwarder == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var mErr *multierror.Error
|
||||||
|
if err := m.dropDNSFirewall(); err != nil {
|
||||||
|
mErr = multierror.Append(mErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.dnsForwarder.Close(ctx); err != nil {
|
||||||
|
mErr = multierror.Append(mErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.dnsForwarder = nil
|
||||||
|
return nberrors.FormatErrorOrNil(mErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Manager) allowDNSFirewall() error {
|
||||||
|
dport := &firewall.Port{
|
||||||
|
IsRange: false,
|
||||||
|
Values: []int{ListenPort},
|
||||||
|
}
|
||||||
|
dnsRules, err := h.firewall.AddPeerFiltering(net.ParseIP("0.0.0.0"), firewall.ProtocolUDP, nil, dport, firewall.RuleDirectionIN, firewall.ActionAccept, "", "")
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to add allow DNS router rules, err: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.fwRules = dnsRules
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Manager) dropDNSFirewall() error {
|
||||||
|
var mErr *multierror.Error
|
||||||
|
for _, rule := range h.fwRules {
|
||||||
|
if err := h.firewall.DeletePeerRule(rule); err != nil {
|
||||||
|
mErr = multierror.Append(mErr, fmt.Errorf("failed to delete DNS router rules, err: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.fwRules = nil
|
||||||
|
return nberrors.FormatErrorOrNil(mErr)
|
||||||
|
}
|
||||||
@@ -4,13 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/pion/stun/v2"
|
"github.com/pion/stun/v2"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/firewall"
|
"github.com/netbirdio/netbird/client/firewall"
|
||||||
"github.com/netbirdio/netbird/client/firewall/manager"
|
"github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
@@ -28,16 +29,18 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
"github.com/netbirdio/netbird/client/internal/acl"
|
"github.com/netbirdio/netbird/client/internal/acl"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns"
|
"github.com/netbirdio/netbird/client/internal/dns"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dnsfwd"
|
||||||
"github.com/netbirdio/netbird/client/internal/networkmonitor"
|
"github.com/netbirdio/netbird/client/internal/networkmonitor"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer/guard"
|
"github.com/netbirdio/netbird/client/internal/peer/guard"
|
||||||
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peerstore"
|
||||||
"github.com/netbirdio/netbird/client/internal/relay"
|
"github.com/netbirdio/netbird/client/internal/relay"
|
||||||
"github.com/netbirdio/netbird/client/internal/rosenpass"
|
"github.com/netbirdio/netbird/client/internal/rosenpass"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager"
|
"github.com/netbirdio/netbird/client/internal/routemanager"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
|
semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group"
|
||||||
|
|
||||||
nbssh "github.com/netbirdio/netbird/client/ssh"
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
@@ -61,6 +64,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
PeerConnectionTimeoutMax = 45000 // ms
|
PeerConnectionTimeoutMax = 45000 // ms
|
||||||
PeerConnectionTimeoutMin = 30000 // ms
|
PeerConnectionTimeoutMin = 30000 // ms
|
||||||
|
connInitLimit = 200
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrResetConnection = fmt.Errorf("reset connection")
|
var ErrResetConnection = fmt.Errorf("reset connection")
|
||||||
@@ -114,7 +118,7 @@ type Engine struct {
|
|||||||
// mgmClient is a Management Service client
|
// mgmClient is a Management Service client
|
||||||
mgmClient mgm.Client
|
mgmClient mgm.Client
|
||||||
// peerConns is a map that holds all the peers that are known to this peer
|
// peerConns is a map that holds all the peers that are known to this peer
|
||||||
peerConns map[string]*peer.Conn
|
peerStore *peerstore.Store
|
||||||
|
|
||||||
beforePeerHook nbnet.AddHookFunc
|
beforePeerHook nbnet.AddHookFunc
|
||||||
afterPeerHook nbnet.RemoveHookFunc
|
afterPeerHook nbnet.RemoveHookFunc
|
||||||
@@ -134,10 +138,6 @@ type Engine struct {
|
|||||||
TURNs []*stun.URI
|
TURNs []*stun.URI
|
||||||
stunTurn atomic.Value
|
stunTurn atomic.Value
|
||||||
|
|
||||||
// clientRoutes is the most recent list of clientRoutes received from the Management Service
|
|
||||||
clientRoutes route.HAMap
|
|
||||||
clientRoutesMu sync.RWMutex
|
|
||||||
|
|
||||||
clientCtx context.Context
|
clientCtx context.Context
|
||||||
clientCancel context.CancelFunc
|
clientCancel context.CancelFunc
|
||||||
|
|
||||||
@@ -158,9 +158,10 @@ type Engine struct {
|
|||||||
|
|
||||||
statusRecorder *peer.Status
|
statusRecorder *peer.Status
|
||||||
|
|
||||||
firewall manager.Manager
|
firewall manager.Manager
|
||||||
routeManager routemanager.Manager
|
routeManager routemanager.Manager
|
||||||
acl acl.Manager
|
acl acl.Manager
|
||||||
|
dnsForwardMgr *dnsfwd.Manager
|
||||||
|
|
||||||
dnsServer dns.Server
|
dnsServer dns.Server
|
||||||
|
|
||||||
@@ -171,7 +172,12 @@ type Engine struct {
|
|||||||
|
|
||||||
relayManager *relayClient.Manager
|
relayManager *relayClient.Manager
|
||||||
stateManager *statemanager.Manager
|
stateManager *statemanager.Manager
|
||||||
srWatcher *guard.SRWatcher
|
srWatcher *guard.SRWatcher
|
||||||
|
|
||||||
|
// Network map persistence
|
||||||
|
persistNetworkMap bool
|
||||||
|
latestNetworkMap *mgmProto.NetworkMap
|
||||||
|
connSemaphore *semaphoregroup.SemaphoreGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// Peer is an instance of the Connection Peer
|
// Peer is an instance of the Connection Peer
|
||||||
@@ -226,7 +232,7 @@ func NewEngineWithProbes(
|
|||||||
signaler: peer.NewSignaler(signalClient, config.WgPrivateKey),
|
signaler: peer.NewSignaler(signalClient, config.WgPrivateKey),
|
||||||
mgmClient: mgmClient,
|
mgmClient: mgmClient,
|
||||||
relayManager: relayManager,
|
relayManager: relayManager,
|
||||||
peerConns: make(map[string]*peer.Conn),
|
peerStore: peerstore.NewConnStore(),
|
||||||
syncMsgMux: &sync.Mutex{},
|
syncMsgMux: &sync.Mutex{},
|
||||||
config: config,
|
config: config,
|
||||||
mobileDep: mobileDep,
|
mobileDep: mobileDep,
|
||||||
@@ -237,6 +243,18 @@ func NewEngineWithProbes(
|
|||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
probes: probes,
|
probes: probes,
|
||||||
checks: checks,
|
checks: checks,
|
||||||
|
connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit),
|
||||||
|
}
|
||||||
|
if runtime.GOOS == "ios" {
|
||||||
|
if !fileExists(mobileDep.StateFilePath) {
|
||||||
|
err := createFile(mobileDep.StateFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create state file: %v", err)
|
||||||
|
// we are not exiting as we can run without the state manager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.stateManager = statemanager.New(mobileDep.StateFilePath)
|
||||||
}
|
}
|
||||||
if path := statemanager.GetDefaultStatePath(); path != "" {
|
if path := statemanager.GetDefaultStatePath(); path != "" {
|
||||||
engine.stateManager = statemanager.New(path)
|
engine.stateManager = statemanager.New(path)
|
||||||
@@ -267,19 +285,26 @@ func (e *Engine) Stop() error {
|
|||||||
e.routeManager.Stop(e.stateManager)
|
e.routeManager.Stop(e.stateManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.dnsForwardMgr != nil {
|
||||||
|
if err := e.dnsForwardMgr.Stop(context.Background()); err != nil {
|
||||||
|
log.Errorf("failed to stop DNS forward: %v", err)
|
||||||
|
}
|
||||||
|
e.dnsForwardMgr = nil
|
||||||
|
}
|
||||||
|
|
||||||
if e.srWatcher != nil {
|
if e.srWatcher != nil {
|
||||||
e.srWatcher.Close()
|
e.srWatcher.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.statusRecorder.ReplaceOfflinePeers([]peer.State{})
|
||||||
|
e.statusRecorder.UpdateDNSStates([]peer.NSGroupState{})
|
||||||
|
e.statusRecorder.UpdateRelayStates([]relay.ProbeResult{})
|
||||||
|
|
||||||
err := e.removeAllPeers()
|
err := e.removeAllPeers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to remove all peers: %s", err)
|
return fmt.Errorf("failed to remove all peers: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.clientRoutesMu.Lock()
|
|
||||||
e.clientRoutes = nil
|
|
||||||
e.clientRoutesMu.Unlock()
|
|
||||||
|
|
||||||
if e.cancel != nil {
|
if e.cancel != nil {
|
||||||
e.cancel()
|
e.cancel()
|
||||||
}
|
}
|
||||||
@@ -297,7 +322,7 @@ func (e *Engine) Stop() error {
|
|||||||
if err := e.stateManager.Stop(ctx); err != nil {
|
if err := e.stateManager.Stop(ctx); err != nil {
|
||||||
return fmt.Errorf("failed to stop state manager: %w", err)
|
return fmt.Errorf("failed to stop state manager: %w", err)
|
||||||
}
|
}
|
||||||
if err := e.stateManager.PersistState(ctx); err != nil {
|
if err := e.stateManager.PersistState(context.Background()); err != nil {
|
||||||
log.Errorf("failed to persist state: %v", err)
|
log.Errorf("failed to persist state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,8 +374,19 @@ func (e *Engine) Start() error {
|
|||||||
}
|
}
|
||||||
e.dnsServer = dnsServer
|
e.dnsServer = dnsServer
|
||||||
|
|
||||||
e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.config.DNSRouteInterval, e.wgInterface, e.statusRecorder, e.relayManager, initialRoutes)
|
e.routeManager = routemanager.NewManager(
|
||||||
beforePeerHook, afterPeerHook, err := e.routeManager.Init(e.stateManager)
|
e.ctx,
|
||||||
|
e.config.WgPrivateKey.PublicKey().String(),
|
||||||
|
e.config.DNSRouteInterval,
|
||||||
|
e.wgInterface,
|
||||||
|
e.statusRecorder,
|
||||||
|
e.relayManager,
|
||||||
|
initialRoutes,
|
||||||
|
e.stateManager,
|
||||||
|
dnsServer,
|
||||||
|
e.peerStore,
|
||||||
|
)
|
||||||
|
beforePeerHook, afterPeerHook, err := e.routeManager.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to initialize route manager: %s", err)
|
log.Errorf("Failed to initialize route manager: %s", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -427,8 +463,8 @@ func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
|||||||
var modified []*mgmProto.RemotePeerConfig
|
var modified []*mgmProto.RemotePeerConfig
|
||||||
for _, p := range peersUpdate {
|
for _, p := range peersUpdate {
|
||||||
peerPubKey := p.GetWgPubKey()
|
peerPubKey := p.GetWgPubKey()
|
||||||
if peerConn, ok := e.peerConns[peerPubKey]; ok {
|
if allowedIPs, ok := e.peerStore.AllowedIPs(peerPubKey); ok {
|
||||||
if peerConn.WgConfig().AllowedIps != strings.Join(p.AllowedIps, ",") {
|
if allowedIPs != strings.Join(p.AllowedIps, ",") {
|
||||||
modified = append(modified, p)
|
modified = append(modified, p)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -459,17 +495,12 @@ func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
|||||||
// removePeers finds and removes peers that do not exist anymore in the network map received from the Management Service.
|
// removePeers finds and removes peers that do not exist anymore in the network map received from the Management Service.
|
||||||
// It also removes peers that have been modified (e.g. change of IP address). They will be added again in addPeers method.
|
// It also removes peers that have been modified (e.g. change of IP address). They will be added again in addPeers method.
|
||||||
func (e *Engine) removePeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
func (e *Engine) removePeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
||||||
currentPeers := make([]string, 0, len(e.peerConns))
|
|
||||||
for p := range e.peerConns {
|
|
||||||
currentPeers = append(currentPeers, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
newPeers := make([]string, 0, len(peersUpdate))
|
newPeers := make([]string, 0, len(peersUpdate))
|
||||||
for _, p := range peersUpdate {
|
for _, p := range peersUpdate {
|
||||||
newPeers = append(newPeers, p.GetWgPubKey())
|
newPeers = append(newPeers, p.GetWgPubKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
toRemove := util.SliceDiff(currentPeers, newPeers)
|
toRemove := util.SliceDiff(e.peerStore.PeersPubKey(), newPeers)
|
||||||
|
|
||||||
for _, p := range toRemove {
|
for _, p := range toRemove {
|
||||||
err := e.removePeer(p)
|
err := e.removePeer(p)
|
||||||
@@ -483,7 +514,7 @@ func (e *Engine) removePeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
|||||||
|
|
||||||
func (e *Engine) removeAllPeers() error {
|
func (e *Engine) removeAllPeers() error {
|
||||||
log.Debugf("removing all peer connections")
|
log.Debugf("removing all peer connections")
|
||||||
for p := range e.peerConns {
|
for _, p := range e.peerStore.PeersPubKey() {
|
||||||
err := e.removePeer(p)
|
err := e.removePeer(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -507,9 +538,8 @@ func (e *Engine) removePeer(peerKey string) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
conn, exists := e.peerConns[peerKey]
|
conn, exists := e.peerStore.Remove(peerKey)
|
||||||
if exists {
|
if exists {
|
||||||
delete(e.peerConns, peerKey)
|
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -538,6 +568,7 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
|
|
||||||
relayMsg := wCfg.GetRelay()
|
relayMsg := wCfg.GetRelay()
|
||||||
if relayMsg != nil {
|
if relayMsg != nil {
|
||||||
|
// when we receive token we expect valid address list too
|
||||||
c := &auth.Token{
|
c := &auth.Token{
|
||||||
Payload: relayMsg.GetTokenPayload(),
|
Payload: relayMsg.GetTokenPayload(),
|
||||||
Signature: relayMsg.GetTokenSignature(),
|
Signature: relayMsg.GetTokenSignature(),
|
||||||
@@ -546,9 +577,16 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
log.Errorf("failed to update relay token: %v", err)
|
log.Errorf("failed to update relay token: %v", err)
|
||||||
return fmt.Errorf("update relay token: %w", err)
|
return fmt.Errorf("update relay token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.relayManager.UpdateServerURLs(relayMsg.Urls)
|
||||||
|
|
||||||
|
// Just in case the agent started with an MGM server where the relay was disabled but was later enabled.
|
||||||
|
// We can ignore all errors because the guard will manage the reconnection retries.
|
||||||
|
_ = e.relayManager.Serve()
|
||||||
|
} else {
|
||||||
|
e.relayManager.UpdateServerURLs(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo update relay address in the relay manager
|
|
||||||
// todo update signal
|
// todo update signal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,13 +594,22 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if update.GetNetworkMap() != nil {
|
nm := update.GetNetworkMap()
|
||||||
// only apply new changes and ignore old ones
|
if nm == nil {
|
||||||
err := e.updateNetworkMap(update.GetNetworkMap())
|
return nil
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store network map if persistence is enabled
|
||||||
|
if e.persistNetworkMap {
|
||||||
|
e.latestNetworkMap = nm
|
||||||
|
log.Debugf("network map persisted with serial %d", nm.GetSerial())
|
||||||
|
}
|
||||||
|
|
||||||
|
// only apply new changes and ignore old ones
|
||||||
|
if err := e.updateNetworkMap(nm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,6 +688,10 @@ func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
|
func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
|
||||||
|
if e.wgInterface == nil {
|
||||||
|
return errors.New("wireguard interface is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
if e.wgInterface.Address().String() != conf.Address {
|
if e.wgInterface.Address().String() != conf.Address {
|
||||||
oldAddr := e.wgInterface.Address().String()
|
oldAddr := e.wgInterface.Address().String()
|
||||||
log.Debugf("updating peer address from %s to %s", oldAddr, conf.Address)
|
log.Debugf("updating peer address from %s to %s", oldAddr, conf.Address)
|
||||||
@@ -732,7 +783,6 @@ func (e *Engine) updateTURNs(turns []*mgmProto.ProtectedHostConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
||||||
|
|
||||||
// intentionally leave it before checking serial because for now it can happen that peer IP changed but serial didn't
|
// intentionally leave it before checking serial because for now it can happen that peer IP changed but serial didn't
|
||||||
if networkMap.GetPeerConfig() != nil {
|
if networkMap.GetPeerConfig() != nil {
|
||||||
err := e.updateConfig(networkMap.GetPeerConfig())
|
err := e.updateConfig(networkMap.GetPeerConfig())
|
||||||
@@ -752,19 +802,17 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
|||||||
e.acl.ApplyFiltering(networkMap)
|
e.acl.ApplyFiltering(networkMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
protoRoutes := networkMap.GetRoutes()
|
var dnsRouteFeatureFlag bool
|
||||||
if protoRoutes == nil {
|
if networkMap.PeerConfig != nil {
|
||||||
protoRoutes = []*mgmProto.Route{}
|
dnsRouteFeatureFlag = networkMap.PeerConfig.RoutingPeerDnsResolutionEnabled
|
||||||
}
|
}
|
||||||
|
routedDomains, routes := toRoutes(networkMap.GetRoutes())
|
||||||
|
|
||||||
_, clientRoutes, err := e.routeManager.UpdateRoutes(serial, toRoutes(protoRoutes))
|
if err := e.routeManager.UpdateRoutes(serial, routes, dnsRouteFeatureFlag); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to update clientRoutes, err: %v", err)
|
log.Errorf("failed to update clientRoutes, err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.clientRoutesMu.Lock()
|
e.updateDNSForwarder(dnsRouteFeatureFlag, routedDomains)
|
||||||
e.clientRoutes = clientRoutes
|
|
||||||
e.clientRoutesMu.Unlock()
|
|
||||||
|
|
||||||
log.Debugf("got peers update from Management Service, total peers to connect to = %d", len(networkMap.GetRemotePeers()))
|
log.Debugf("got peers update from Management Service, total peers to connect to = %d", len(networkMap.GetRemotePeers()))
|
||||||
|
|
||||||
@@ -813,8 +861,7 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
|||||||
protoDNSConfig = &mgmProto.DNSConfig{}
|
protoDNSConfig = &mgmProto.DNSConfig{}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig))
|
if err := e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig)); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to update dns server, err: %v", err)
|
log.Errorf("failed to update dns server, err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,7 +874,12 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func toRoutes(protoRoutes []*mgmProto.Route) []*route.Route {
|
func toRoutes(protoRoutes []*mgmProto.Route) ([]string, []*route.Route) {
|
||||||
|
if protoRoutes == nil {
|
||||||
|
protoRoutes = []*mgmProto.Route{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dnsRoutes []string
|
||||||
routes := make([]*route.Route, 0)
|
routes := make([]*route.Route, 0)
|
||||||
for _, protoRoute := range protoRoutes {
|
for _, protoRoute := range protoRoutes {
|
||||||
var prefix netip.Prefix
|
var prefix netip.Prefix
|
||||||
@@ -838,6 +890,8 @@ func toRoutes(protoRoutes []*mgmProto.Route) []*route.Route {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dnsRoutes = append(dnsRoutes, protoRoute.Domains...)
|
||||||
|
|
||||||
convertedRoute := &route.Route{
|
convertedRoute := &route.Route{
|
||||||
ID: route.ID(protoRoute.ID),
|
ID: route.ID(protoRoute.ID),
|
||||||
Network: prefix,
|
Network: prefix,
|
||||||
@@ -851,7 +905,7 @@ func toRoutes(protoRoutes []*mgmProto.Route) []*route.Route {
|
|||||||
}
|
}
|
||||||
routes = append(routes, convertedRoute)
|
routes = append(routes, convertedRoute)
|
||||||
}
|
}
|
||||||
return routes
|
return dnsRoutes, routes
|
||||||
}
|
}
|
||||||
|
|
||||||
func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig) nbdns.Config {
|
func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig) nbdns.Config {
|
||||||
@@ -928,12 +982,16 @@ func (e *Engine) addNewPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
|||||||
func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
|
func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
|
||||||
peerKey := peerConfig.GetWgPubKey()
|
peerKey := peerConfig.GetWgPubKey()
|
||||||
peerIPs := peerConfig.GetAllowedIps()
|
peerIPs := peerConfig.GetAllowedIps()
|
||||||
if _, ok := e.peerConns[peerKey]; !ok {
|
if _, ok := e.peerStore.PeerConn(peerKey); !ok {
|
||||||
conn, err := e.createPeerConn(peerKey, strings.Join(peerIPs, ","))
|
conn, err := e.createPeerConn(peerKey, strings.Join(peerIPs, ","))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create peer connection: %w", err)
|
return fmt.Errorf("create peer connection: %w", err)
|
||||||
}
|
}
|
||||||
e.peerConns[peerKey] = conn
|
|
||||||
|
if ok := e.peerStore.AddPeerConn(peerKey, conn); !ok {
|
||||||
|
conn.Close()
|
||||||
|
return fmt.Errorf("peer already exists: %s", peerKey)
|
||||||
|
}
|
||||||
|
|
||||||
if e.beforePeerHook != nil && e.afterPeerHook != nil {
|
if e.beforePeerHook != nil && e.afterPeerHook != nil {
|
||||||
conn.AddBeforeAddPeerHook(e.beforePeerHook)
|
conn.AddBeforeAddPeerHook(e.beforePeerHook)
|
||||||
@@ -1001,7 +1059,7 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, e
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConn, err := peer.NewConn(e.ctx, config, e.statusRecorder, e.signaler, e.mobileDep.IFaceDiscover, e.relayManager, e.srWatcher)
|
peerConn, err := peer.NewConn(e.ctx, config, e.statusRecorder, e.signaler, e.mobileDep.IFaceDiscover, e.relayManager, e.srWatcher, e.connSemaphore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1022,8 +1080,8 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
conn := e.peerConns[msg.Key]
|
conn, ok := e.peerStore.PeerConn(msg.Key)
|
||||||
if conn == nil {
|
if !ok {
|
||||||
return fmt.Errorf("wrongly addressed message %s", msg.Key)
|
return fmt.Errorf("wrongly addressed message %s", msg.Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1081,7 +1139,7 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go conn.OnRemoteCandidate(candidate, e.GetClientRoutes())
|
go conn.OnRemoteCandidate(candidate, e.routeManager.GetClientRoutes())
|
||||||
case sProto.Body_MODE:
|
case sProto.Body_MODE:
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1185,7 +1243,7 @@ func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
routes := toRoutes(netMap.GetRoutes())
|
_, routes := toRoutes(netMap.GetRoutes())
|
||||||
dnsCfg := toDNSConfig(netMap.GetDNSConfig())
|
dnsCfg := toDNSConfig(netMap.GetDNSConfig())
|
||||||
return routes, &dnsCfg, nil
|
return routes, &dnsCfg, nil
|
||||||
}
|
}
|
||||||
@@ -1268,26 +1326,6 @@ func (e *Engine) newDnsServer() ([]*route.Route, dns.Server, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClientRoutes returns the current routes from the route map
|
|
||||||
func (e *Engine) GetClientRoutes() route.HAMap {
|
|
||||||
e.clientRoutesMu.RLock()
|
|
||||||
defer e.clientRoutesMu.RUnlock()
|
|
||||||
|
|
||||||
return maps.Clone(e.clientRoutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetClientRoutesWithNetID returns the current routes from the route map, but the keys consist of the network ID only
|
|
||||||
func (e *Engine) GetClientRoutesWithNetID() map[route.NetID][]*route.Route {
|
|
||||||
e.clientRoutesMu.RLock()
|
|
||||||
defer e.clientRoutesMu.RUnlock()
|
|
||||||
|
|
||||||
routes := make(map[route.NetID][]*route.Route, len(e.clientRoutes))
|
|
||||||
for id, v := range e.clientRoutes {
|
|
||||||
routes[id.NetID()] = v
|
|
||||||
}
|
|
||||||
return routes
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRouteManager returns the route manager
|
// GetRouteManager returns the route manager
|
||||||
func (e *Engine) GetRouteManager() routemanager.Manager {
|
func (e *Engine) GetRouteManager() routemanager.Manager {
|
||||||
return e.routeManager
|
return e.routeManager
|
||||||
@@ -1372,9 +1410,8 @@ func (e *Engine) receiveProbeEvents() {
|
|||||||
go e.probes.WgProbe.Receive(e.ctx, func() bool {
|
go e.probes.WgProbe.Receive(e.ctx, func() bool {
|
||||||
log.Debug("received wg probe request")
|
log.Debug("received wg probe request")
|
||||||
|
|
||||||
for _, peer := range e.peerConns {
|
for _, key := range e.peerStore.PeersPubKey() {
|
||||||
key := peer.GetKey()
|
wgStats, err := e.wgInterface.GetStats(key)
|
||||||
wgStats, err := peer.WgConfig().WgInterface.GetStats(key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("failed to get wg stats for peer %s: %s", key, err)
|
log.Debugf("failed to get wg stats for peer %s: %s", key, err)
|
||||||
}
|
}
|
||||||
@@ -1451,7 +1488,7 @@ func (e *Engine) startNetworkMonitor() {
|
|||||||
|
|
||||||
func (e *Engine) addrViaRoutes(addr netip.Addr) (bool, netip.Prefix, error) {
|
func (e *Engine) addrViaRoutes(addr netip.Addr) (bool, netip.Prefix, error) {
|
||||||
var vpnRoutes []netip.Prefix
|
var vpnRoutes []netip.Prefix
|
||||||
for _, routes := range e.GetClientRoutes() {
|
for _, routes := range e.routeManager.GetClientRoutes() {
|
||||||
if len(routes) > 0 && routes[0] != nil {
|
if len(routes) > 0 && routes[0] != nil {
|
||||||
vpnRoutes = append(vpnRoutes, routes[0].Network)
|
vpnRoutes = append(vpnRoutes, routes[0].Network)
|
||||||
}
|
}
|
||||||
@@ -1479,8 +1516,93 @@ func (e *Engine) stopDNSServer() {
|
|||||||
e.statusRecorder.UpdateDNSStates(nsGroupStates)
|
e.statusRecorder.UpdateDNSStates(nsGroupStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetNetworkMapPersistence enables or disables network map persistence
|
||||||
|
func (e *Engine) SetNetworkMapPersistence(enabled bool) {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
if enabled == e.persistNetworkMap {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.persistNetworkMap = enabled
|
||||||
|
log.Debugf("Network map persistence is set to %t", enabled)
|
||||||
|
|
||||||
|
if !enabled {
|
||||||
|
e.latestNetworkMap = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestNetworkMap returns the stored network map if persistence is enabled
|
||||||
|
func (e *Engine) GetLatestNetworkMap() (*mgmProto.NetworkMap, error) {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
if !e.persistNetworkMap {
|
||||||
|
return nil, errors.New("network map persistence is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.latestNetworkMap == nil {
|
||||||
|
//nolint:nilnil
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a deep copy to avoid external modifications
|
||||||
|
nm, ok := proto.Clone(e.latestNetworkMap).(*mgmProto.NetworkMap)
|
||||||
|
if !ok {
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to clone network map")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateDNSForwarder start or stop the DNS forwarder based on the domains and the feature flag
|
||||||
|
func (e *Engine) updateDNSForwarder(enabled bool, domains []string) {
|
||||||
|
if !enabled {
|
||||||
|
if e.dnsForwardMgr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := e.dnsForwardMgr.Stop(context.Background()); err != nil {
|
||||||
|
log.Errorf("failed to stop DNS forward: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(domains) > 0 {
|
||||||
|
log.Infof("enable domain router service for domains: %v", domains)
|
||||||
|
if e.dnsForwardMgr == nil {
|
||||||
|
e.dnsForwardMgr = dnsfwd.NewManager(e.firewall)
|
||||||
|
|
||||||
|
if err := e.dnsForwardMgr.Start(domains); err != nil {
|
||||||
|
log.Errorf("failed to start DNS forward: %v", err)
|
||||||
|
e.dnsForwardMgr = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("update domain router service for domains: %v", domains)
|
||||||
|
e.dnsForwardMgr.UpdateDomains(domains)
|
||||||
|
}
|
||||||
|
} else if e.dnsForwardMgr != nil {
|
||||||
|
log.Infof("disable domain router service")
|
||||||
|
if err := e.dnsForwardMgr.Stop(context.Background()); err != nil {
|
||||||
|
log.Errorf("failed to stop DNS forward: %v", err)
|
||||||
|
}
|
||||||
|
e.dnsForwardMgr = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// isChecksEqual checks if two slices of checks are equal.
|
// isChecksEqual checks if two slices of checks are equal.
|
||||||
func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool {
|
func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool {
|
||||||
|
for _, check := range checks {
|
||||||
|
sort.Slice(check.Files, func(i, j int) bool {
|
||||||
|
return check.Files[i] < check.Files[j]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, oCheck := range oChecks {
|
||||||
|
sort.Slice(oCheck.Files, func(i, j int) bool {
|
||||||
|
return oCheck.Files[i] < oCheck.Files[j]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return slices.EqualFunc(checks, oChecks, func(checks, oChecks *mgmProto.Checks) bool {
|
return slices.EqualFunc(checks, oChecks, func(checks, oChecks *mgmProto.Checks) bool {
|
||||||
return slices.Equal(checks.Files, oChecks.Files)
|
return slices.Equal(checks.Files, oChecks.Files)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import (
|
|||||||
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
||||||
"github.com/netbirdio/netbird/management/server"
|
"github.com/netbirdio/netbird/management/server"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
relayClient "github.com/netbirdio/netbird/relay/client"
|
relayClient "github.com/netbirdio/netbird/relay/client"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
@@ -245,12 +247,15 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
nil)
|
nil)
|
||||||
|
|
||||||
wgIface := &iface.MockWGIface{
|
wgIface := &iface.MockWGIface{
|
||||||
|
NameFunc: func() string { return "utun102" },
|
||||||
RemovePeerFunc: func(peerKey string) error {
|
RemovePeerFunc: func(peerKey string) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
engine.wgInterface = wgIface
|
engine.wgInterface = wgIface
|
||||||
engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), time.Minute, engine.wgInterface, engine.statusRecorder, relayMgr, nil)
|
engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), time.Minute, engine.wgInterface, engine.statusRecorder, relayMgr, nil, nil, nil, nil)
|
||||||
|
_, _, err = engine.routeManager.Init()
|
||||||
|
require.NoError(t, err)
|
||||||
engine.dnsServer = &dns.MockServer{
|
engine.dnsServer = &dns.MockServer{
|
||||||
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
|
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
|
||||||
}
|
}
|
||||||
@@ -388,8 +393,8 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(engine.peerConns) != c.expectedLen {
|
if len(engine.peerStore.PeersPubKey()) != c.expectedLen {
|
||||||
t.Errorf("expecting Engine.peerConns to be of size %d, got %d", c.expectedLen, len(engine.peerConns))
|
t.Errorf("expecting Engine.peerConns to be of size %d, got %d", c.expectedLen, len(engine.peerStore.PeersPubKey()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if engine.networkSerial != c.expectedSerial {
|
if engine.networkSerial != c.expectedSerial {
|
||||||
@@ -397,7 +402,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range c.expectedPeers {
|
for _, p := range c.expectedPeers {
|
||||||
conn, ok := engine.peerConns[p.GetWgPubKey()]
|
conn, ok := engine.peerStore.PeerConn(p.GetWgPubKey())
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("expecting Engine.peerConns to contain peer %s", p)
|
t.Errorf("expecting Engine.peerConns to contain peer %s", p)
|
||||||
}
|
}
|
||||||
@@ -622,10 +627,10 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
|
|||||||
}{}
|
}{}
|
||||||
|
|
||||||
mockRouteManager := &routemanager.MockManager{
|
mockRouteManager := &routemanager.MockManager{
|
||||||
UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, error) {
|
UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) error {
|
||||||
input.inputSerial = updateSerial
|
input.inputSerial = updateSerial
|
||||||
input.inputRoutes = newRoutes
|
input.inputRoutes = newRoutes
|
||||||
return nil, nil, testCase.inputErr
|
return testCase.inputErr
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,8 +803,8 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
|
|||||||
assert.NoError(t, err, "shouldn't return error")
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
mockRouteManager := &routemanager.MockManager{
|
mockRouteManager := &routemanager.MockManager{
|
||||||
UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, error) {
|
UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) error {
|
||||||
return nil, nil, nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1006,6 +1011,99 @@ func Test_ParseNATExternalIPMappings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_CheckFilesEqual(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
inputChecks1 []*mgmtProto.Checks
|
||||||
|
inputChecks2 []*mgmtProto.Checks
|
||||||
|
expectedBool bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Equal Files In Equal Order Should Return True",
|
||||||
|
inputChecks1: []*mgmtProto.Checks{
|
||||||
|
{
|
||||||
|
Files: []string{
|
||||||
|
"testfile1",
|
||||||
|
"testfile2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputChecks2: []*mgmtProto.Checks{
|
||||||
|
{
|
||||||
|
Files: []string{
|
||||||
|
"testfile1",
|
||||||
|
"testfile2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedBool: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Equal Files In Reverse Order Should Return True",
|
||||||
|
inputChecks1: []*mgmtProto.Checks{
|
||||||
|
{
|
||||||
|
Files: []string{
|
||||||
|
"testfile1",
|
||||||
|
"testfile2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputChecks2: []*mgmtProto.Checks{
|
||||||
|
{
|
||||||
|
Files: []string{
|
||||||
|
"testfile2",
|
||||||
|
"testfile1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedBool: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unequal Files Should Return False",
|
||||||
|
inputChecks1: []*mgmtProto.Checks{
|
||||||
|
{
|
||||||
|
Files: []string{
|
||||||
|
"testfile1",
|
||||||
|
"testfile2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputChecks2: []*mgmtProto.Checks{
|
||||||
|
{
|
||||||
|
Files: []string{
|
||||||
|
"testfile1",
|
||||||
|
"testfile3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedBool: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Compared With Empty Should Return False",
|
||||||
|
inputChecks1: []*mgmtProto.Checks{
|
||||||
|
{
|
||||||
|
Files: []string{
|
||||||
|
"testfile1",
|
||||||
|
"testfile2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputChecks2: []*mgmtProto.Checks{
|
||||||
|
{
|
||||||
|
Files: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedBool: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
result := isChecksEqual(testCase.inputChecks1, testCase.inputChecks2)
|
||||||
|
assert.Equal(t, testCase.expectedBool, result, "result should match expected bool")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey string, i int, mgmtAddr string, signalAddr string) (*Engine, error) {
|
func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey string, i int, mgmtAddr string, signalAddr string) (*Engine, error) {
|
||||||
key, err := wgtypes.GeneratePrivateKey()
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1100,7 +1198,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
|||||||
}
|
}
|
||||||
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
||||||
|
|
||||||
store, cleanUp, err := server.NewTestStoreFromSQL(context.Background(), testFile, config.Datadir)
|
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), testFile, config.Datadir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
@@ -1122,7 +1220,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
|
secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
|
||||||
mgmtServer, err := server.NewServer(context.Background(), config, accountManager, peersUpdateManager, secretsManager, nil, nil)
|
mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
@@ -1141,7 +1239,8 @@ func getConnectedPeers(e *Engine) int {
|
|||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
i := 0
|
i := 0
|
||||||
for _, conn := range e.peerConns {
|
for _, id := range e.peerStore.PeersPubKey() {
|
||||||
|
conn, _ := e.peerStore.PeerConn(id)
|
||||||
if conn.Status() == peer.StatusConnected {
|
if conn.Status() == peer.StatusConnected {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
@@ -1153,5 +1252,5 @@ func getPeers(e *Engine) int {
|
|||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
return len(e.peerConns)
|
return len(e.peerStore.PeersPubKey())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,4 +19,5 @@ type MobileDependency struct {
|
|||||||
// iOS only
|
// iOS only
|
||||||
DnsManager dns.IosDnsManager
|
DnsManager dns.IosDnsManager
|
||||||
FileDescriptor int32
|
FileDescriptor int32
|
||||||
|
StateFilePath string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
relayClient "github.com/netbirdio/netbird/relay/client"
|
relayClient "github.com/netbirdio/netbird/relay/client"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
|
semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConnPriority int
|
type ConnPriority int
|
||||||
@@ -83,7 +84,6 @@ type Conn struct {
|
|||||||
signaler *Signaler
|
signaler *Signaler
|
||||||
relayManager *relayClient.Manager
|
relayManager *relayClient.Manager
|
||||||
allowedIP net.IP
|
allowedIP net.IP
|
||||||
allowedNet string
|
|
||||||
handshaker *Handshaker
|
handshaker *Handshaker
|
||||||
|
|
||||||
onConnected func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string)
|
onConnected func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string)
|
||||||
@@ -105,13 +105,14 @@ type Conn struct {
|
|||||||
wgProxyICE wgproxy.Proxy
|
wgProxyICE wgproxy.Proxy
|
||||||
wgProxyRelay wgproxy.Proxy
|
wgProxyRelay wgproxy.Proxy
|
||||||
|
|
||||||
guard *guard.Guard
|
guard *guard.Guard
|
||||||
|
semaphore *semaphoregroup.SemaphoreGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConn creates a new not opened Conn to the remote peer.
|
// NewConn creates a new not opened Conn to the remote peer.
|
||||||
// To establish a connection run Conn.Open
|
// To establish a connection run Conn.Open
|
||||||
func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Status, signaler *Signaler, iFaceDiscover stdnet.ExternalIFaceDiscover, relayManager *relayClient.Manager, srWatcher *guard.SRWatcher) (*Conn, error) {
|
func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Status, signaler *Signaler, iFaceDiscover stdnet.ExternalIFaceDiscover, relayManager *relayClient.Manager, srWatcher *guard.SRWatcher, semaphore *semaphoregroup.SemaphoreGroup) (*Conn, error) {
|
||||||
allowedIP, allowedNet, err := net.ParseCIDR(config.WgConfig.AllowedIps)
|
allowedIP, _, err := net.ParseCIDR(config.WgConfig.AllowedIps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to parse allowedIPS: %v", err)
|
log.Errorf("failed to parse allowedIPS: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -129,9 +130,9 @@ func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Statu
|
|||||||
signaler: signaler,
|
signaler: signaler,
|
||||||
relayManager: relayManager,
|
relayManager: relayManager,
|
||||||
allowedIP: allowedIP,
|
allowedIP: allowedIP,
|
||||||
allowedNet: allowedNet.String(),
|
|
||||||
statusRelay: NewAtomicConnStatus(),
|
statusRelay: NewAtomicConnStatus(),
|
||||||
statusICE: NewAtomicConnStatus(),
|
statusICE: NewAtomicConnStatus(),
|
||||||
|
semaphore: semaphore,
|
||||||
}
|
}
|
||||||
|
|
||||||
rFns := WorkerRelayCallbacks{
|
rFns := WorkerRelayCallbacks{
|
||||||
@@ -171,6 +172,7 @@ func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Statu
|
|||||||
// 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() {
|
func (conn *Conn) Open() {
|
||||||
|
conn.semaphore.Add(conn.ctx)
|
||||||
conn.log.Debugf("open connection to peer")
|
conn.log.Debugf("open connection to peer")
|
||||||
|
|
||||||
conn.mu.Lock()
|
conn.mu.Lock()
|
||||||
@@ -193,6 +195,7 @@ func (conn *Conn) Open() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) startHandshakeAndReconnect(ctx context.Context) {
|
func (conn *Conn) startHandshakeAndReconnect(ctx context.Context) {
|
||||||
|
defer conn.semaphore.Done(conn.ctx)
|
||||||
conn.waitInitialRandomSleepTime(ctx)
|
conn.waitInitialRandomSleepTime(ctx)
|
||||||
|
|
||||||
err := conn.handshaker.sendOffer()
|
err := conn.handshaker.sendOffer()
|
||||||
@@ -309,6 +312,11 @@ func (conn *Conn) iCEConnectionIsReady(priority ConnPriority, iceConnInfo ICECon
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if remoteConnNil(conn.log, iceConnInfo.RemoteConn) {
|
||||||
|
conn.log.Errorf("remote ICE connection is nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
conn.log.Debugf("ICE connection is ready")
|
conn.log.Debugf("ICE connection is ready")
|
||||||
|
|
||||||
if conn.currentConnPriority > priority {
|
if conn.currentConnPriority > priority {
|
||||||
@@ -437,7 +445,7 @@ func (conn *Conn) relayConnectionIsReady(rci RelayConnInfo) {
|
|||||||
|
|
||||||
if conn.iceP2PIsActive() {
|
if conn.iceP2PIsActive() {
|
||||||
conn.log.Debugf("do not switch to relay because current priority is: %v", conn.currentConnPriority)
|
conn.log.Debugf("do not switch to relay because current priority is: %v", conn.currentConnPriority)
|
||||||
conn.wgProxyRelay = wgProxy
|
conn.setRelayedProxy(wgProxy)
|
||||||
conn.statusRelay.Set(StatusConnected)
|
conn.statusRelay.Set(StatusConnected)
|
||||||
conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey)
|
conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey)
|
||||||
return
|
return
|
||||||
@@ -460,7 +468,7 @@ func (conn *Conn) relayConnectionIsReady(rci RelayConnInfo) {
|
|||||||
wgConfigWorkaround()
|
wgConfigWorkaround()
|
||||||
conn.currentConnPriority = connPriorityRelay
|
conn.currentConnPriority = connPriorityRelay
|
||||||
conn.statusRelay.Set(StatusConnected)
|
conn.statusRelay.Set(StatusConnected)
|
||||||
conn.wgProxyRelay = wgProxy
|
conn.setRelayedProxy(wgProxy)
|
||||||
conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey)
|
conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey)
|
||||||
conn.log.Infof("start to communicate with peer via relay")
|
conn.log.Infof("start to communicate with peer via relay")
|
||||||
conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr)
|
conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr)
|
||||||
@@ -589,14 +597,13 @@ func (conn *Conn) doOnConnected(remoteRosenpassPubKey []byte, remoteRosenpassAdd
|
|||||||
}
|
}
|
||||||
|
|
||||||
if conn.onConnected != nil {
|
if conn.onConnected != nil {
|
||||||
conn.onConnected(conn.config.Key, remoteRosenpassPubKey, conn.allowedNet, remoteRosenpassAddr)
|
conn.onConnected(conn.config.Key, remoteRosenpassPubKey, conn.allowedIP.String(), remoteRosenpassAddr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) waitInitialRandomSleepTime(ctx context.Context) {
|
func (conn *Conn) waitInitialRandomSleepTime(ctx context.Context) {
|
||||||
minWait := 100
|
maxWait := 300
|
||||||
maxWait := 800
|
duration := time.Duration(rand.Intn(maxWait)) * time.Millisecond
|
||||||
duration := time.Duration(rand.Intn(maxWait-minWait)+minWait) * time.Millisecond
|
|
||||||
|
|
||||||
timeout := time.NewTimer(duration)
|
timeout := time.NewTimer(duration)
|
||||||
defer timeout.Stop()
|
defer timeout.Stop()
|
||||||
@@ -731,6 +738,20 @@ func (conn *Conn) logTraceConnState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (conn *Conn) setRelayedProxy(proxy wgproxy.Proxy) {
|
||||||
|
if conn.wgProxyRelay != nil {
|
||||||
|
if err := conn.wgProxyRelay.CloseConn(); err != nil {
|
||||||
|
conn.log.Warnf("failed to close deprecated wg proxy conn: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn.wgProxyRelay = proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowedIP returns the allowed IP of the remote peer
|
||||||
|
func (conn *Conn) AllowedIP() net.IP {
|
||||||
|
return conn.allowedIP
|
||||||
|
}
|
||||||
|
|
||||||
func isController(config ConnConfig) bool {
|
func isController(config ConnConfig) bool {
|
||||||
return config.LocalKey > config.Key
|
return config.LocalKey > config.Key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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 connConf = ConnConfig{
|
var connConf = ConnConfig{
|
||||||
@@ -46,7 +47,7 @@ func TestNewConn_interfaceFilter(t *testing.T) {
|
|||||||
|
|
||||||
func TestConn_GetKey(t *testing.T) {
|
func TestConn_GetKey(t *testing.T) {
|
||||||
swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig)
|
swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig)
|
||||||
conn, err := NewConn(context.Background(), connConf, nil, nil, nil, nil, swWatcher)
|
conn, err := NewConn(context.Background(), connConf, nil, nil, nil, nil, swWatcher, semaphoregroup.NewSemaphoreGroup(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,7 @@ func TestConn_GetKey(t *testing.T) {
|
|||||||
|
|
||||||
func TestConn_OnRemoteOffer(t *testing.T) {
|
func TestConn_OnRemoteOffer(t *testing.T) {
|
||||||
swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig)
|
swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig)
|
||||||
conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), nil, nil, nil, swWatcher)
|
conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), nil, nil, nil, swWatcher, semaphoregroup.NewSemaphoreGroup(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -92,7 +93,7 @@ func TestConn_OnRemoteOffer(t *testing.T) {
|
|||||||
|
|
||||||
func TestConn_OnRemoteAnswer(t *testing.T) {
|
func TestConn_OnRemoteAnswer(t *testing.T) {
|
||||||
swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig)
|
swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig)
|
||||||
conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), nil, nil, nil, swWatcher)
|
conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), nil, nil, nil, swWatcher, semaphoregroup.NewSemaphoreGroup(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -125,7 +126,7 @@ func TestConn_OnRemoteAnswer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
func TestConn_Status(t *testing.T) {
|
func TestConn_Status(t *testing.T) {
|
||||||
swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig)
|
swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig)
|
||||||
conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), nil, nil, nil, swWatcher)
|
conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), nil, nil, nil, swWatcher, semaphoregroup.NewSemaphoreGroup(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
21
client/internal/peer/nilcheck.go
Normal file
21
client/internal/peer/nilcheck.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func remoteConnNil(log *log.Entry, conn net.Conn) bool {
|
||||||
|
if conn == nil {
|
||||||
|
log.Errorf("ice conn is nil")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn.RemoteAddr() == nil {
|
||||||
|
log.Errorf("ICE remote address is nil")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -17,6 +17,11 @@ import (
|
|||||||
relayClient "github.com/netbirdio/netbird/relay/client"
|
relayClient "github.com/netbirdio/netbird/relay/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ResolvedDomainInfo struct {
|
||||||
|
Prefixes []netip.Prefix
|
||||||
|
ParentDomain domain.Domain
|
||||||
|
}
|
||||||
|
|
||||||
// State contains the latest state of a peer
|
// State contains the latest state of a peer
|
||||||
type State struct {
|
type State struct {
|
||||||
Mux *sync.RWMutex
|
Mux *sync.RWMutex
|
||||||
@@ -67,7 +72,7 @@ func (s *State) DeleteRoute(network string) {
|
|||||||
func (s *State) GetRoutes() map[string]struct{} {
|
func (s *State) GetRoutes() map[string]struct{} {
|
||||||
s.Mux.RLock()
|
s.Mux.RLock()
|
||||||
defer s.Mux.RUnlock()
|
defer s.Mux.RUnlock()
|
||||||
return s.routes
|
return maps.Clone(s.routes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalPeerState contains the latest state of the local peer
|
// LocalPeerState contains the latest state of the local peer
|
||||||
@@ -138,7 +143,7 @@ type Status struct {
|
|||||||
rosenpassEnabled bool
|
rosenpassEnabled bool
|
||||||
rosenpassPermissive bool
|
rosenpassPermissive bool
|
||||||
nsGroupStates []NSGroupState
|
nsGroupStates []NSGroupState
|
||||||
resolvedDomainsStates map[domain.Domain][]netip.Prefix
|
resolvedDomainsStates map[domain.Domain]ResolvedDomainInfo
|
||||||
|
|
||||||
// To reduce the number of notification invocation this bool will be true when need to call the notification
|
// To reduce the number of notification invocation this bool will be true when need to call the notification
|
||||||
// Some Peer actions mostly used by in a batch when the network map has been synchronized. In these type of events
|
// Some Peer actions mostly used by in a batch when the network map has been synchronized. In these type of events
|
||||||
@@ -156,7 +161,7 @@ func NewRecorder(mgmAddress string) *Status {
|
|||||||
offlinePeers: make([]State, 0),
|
offlinePeers: make([]State, 0),
|
||||||
notifier: newNotifier(),
|
notifier: newNotifier(),
|
||||||
mgmAddress: mgmAddress,
|
mgmAddress: mgmAddress,
|
||||||
resolvedDomainsStates: make(map[domain.Domain][]netip.Prefix),
|
resolvedDomainsStates: map[domain.Domain]ResolvedDomainInfo{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,10 +242,6 @@ func (d *Status) UpdatePeerState(receivedState State) error {
|
|||||||
peerState.IP = receivedState.IP
|
peerState.IP = receivedState.IP
|
||||||
}
|
}
|
||||||
|
|
||||||
if receivedState.GetRoutes() != nil {
|
|
||||||
peerState.SetRoutes(receivedState.GetRoutes())
|
|
||||||
}
|
|
||||||
|
|
||||||
skipNotification := shouldSkipNotify(receivedState.ConnStatus, peerState)
|
skipNotification := shouldSkipNotify(receivedState.ConnStatus, peerState)
|
||||||
|
|
||||||
if receivedState.ConnStatus != peerState.ConnStatus {
|
if receivedState.ConnStatus != peerState.ConnStatus {
|
||||||
@@ -261,12 +262,40 @@ func (d *Status) UpdatePeerState(receivedState State) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, found := d.changeNotify[receivedState.PubKey]
|
d.notifyPeerListChanged()
|
||||||
if found && ch != nil {
|
return nil
|
||||||
close(ch)
|
}
|
||||||
d.changeNotify[receivedState.PubKey] = nil
|
|
||||||
|
func (d *Status) AddPeerStateRoute(peer string, route string) error {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
peerState, ok := d.peers[peer]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("peer doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peerState.AddRoute(route)
|
||||||
|
d.peers[peer] = peerState
|
||||||
|
|
||||||
|
// todo: consider to make sense of this notification or not
|
||||||
|
d.notifyPeerListChanged()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Status) RemovePeerStateRoute(peer string, route string) error {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
peerState, ok := d.peers[peer]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("peer doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
peerState.DeleteRoute(route)
|
||||||
|
d.peers[peer] = peerState
|
||||||
|
|
||||||
|
// todo: consider to make sense of this notification or not
|
||||||
d.notifyPeerListChanged()
|
d.notifyPeerListChanged()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -301,12 +330,7 @@ func (d *Status) UpdatePeerICEState(receivedState State) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, found := d.changeNotify[receivedState.PubKey]
|
d.notifyPeerStateChangeListeners(receivedState.PubKey)
|
||||||
if found && ch != nil {
|
|
||||||
close(ch)
|
|
||||||
d.changeNotify[receivedState.PubKey] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
d.notifyPeerListChanged()
|
d.notifyPeerListChanged()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -334,12 +358,7 @@ func (d *Status) UpdatePeerRelayedState(receivedState State) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, found := d.changeNotify[receivedState.PubKey]
|
d.notifyPeerStateChangeListeners(receivedState.PubKey)
|
||||||
if found && ch != nil {
|
|
||||||
close(ch)
|
|
||||||
d.changeNotify[receivedState.PubKey] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
d.notifyPeerListChanged()
|
d.notifyPeerListChanged()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -366,12 +385,7 @@ func (d *Status) UpdatePeerRelayedStateToDisconnected(receivedState State) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, found := d.changeNotify[receivedState.PubKey]
|
d.notifyPeerStateChangeListeners(receivedState.PubKey)
|
||||||
if found && ch != nil {
|
|
||||||
close(ch)
|
|
||||||
d.changeNotify[receivedState.PubKey] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
d.notifyPeerListChanged()
|
d.notifyPeerListChanged()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -401,12 +415,7 @@ func (d *Status) UpdatePeerICEStateToDisconnected(receivedState State) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, found := d.changeNotify[receivedState.PubKey]
|
d.notifyPeerStateChangeListeners(receivedState.PubKey)
|
||||||
if found && ch != nil {
|
|
||||||
close(ch)
|
|
||||||
d.changeNotify[receivedState.PubKey] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
d.notifyPeerListChanged()
|
d.notifyPeerListChanged()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -477,11 +486,14 @@ func (d *Status) FinishPeerListModifications() {
|
|||||||
func (d *Status) GetPeerStateChangeNotifier(peer string) <-chan struct{} {
|
func (d *Status) GetPeerStateChangeNotifier(peer string) <-chan struct{} {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
ch, found := d.changeNotify[peer]
|
ch, found := d.changeNotify[peer]
|
||||||
if !found || ch == nil {
|
if found {
|
||||||
ch = make(chan struct{})
|
return ch
|
||||||
d.changeNotify[peer] = ch
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ch = make(chan struct{})
|
||||||
|
d.changeNotify[peer] = ch
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,16 +596,27 @@ func (d *Status) UpdateDNSStates(dnsStates []NSGroupState) {
|
|||||||
d.nsGroupStates = dnsStates
|
d.nsGroupStates = dnsStates
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) UpdateResolvedDomainsStates(domain domain.Domain, prefixes []netip.Prefix) {
|
func (d *Status) UpdateResolvedDomainsStates(originalDomain domain.Domain, resolvedDomain domain.Domain, prefixes []netip.Prefix) {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.Unlock()
|
||||||
d.resolvedDomainsStates[domain] = prefixes
|
|
||||||
|
// Store both the original domain pattern and resolved domain
|
||||||
|
d.resolvedDomainsStates[resolvedDomain] = ResolvedDomainInfo{
|
||||||
|
Prefixes: prefixes,
|
||||||
|
ParentDomain: originalDomain,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) DeleteResolvedDomainsStates(domain domain.Domain) {
|
func (d *Status) DeleteResolvedDomainsStates(domain domain.Domain) {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.Unlock()
|
||||||
delete(d.resolvedDomainsStates, domain)
|
|
||||||
|
// Remove all entries that have this domain as their parent
|
||||||
|
for k, v := range d.resolvedDomainsStates {
|
||||||
|
if v.ParentDomain == domain {
|
||||||
|
delete(d.resolvedDomainsStates, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetRosenpassState() RosenpassState {
|
func (d *Status) GetRosenpassState() RosenpassState {
|
||||||
@@ -669,25 +692,23 @@ func (d *Status) GetRelayStates() []relay.ProbeResult {
|
|||||||
// extend the list of stun, turn servers with relay address
|
// extend the list of stun, turn servers with relay address
|
||||||
relayStates := slices.Clone(d.relayStates)
|
relayStates := slices.Clone(d.relayStates)
|
||||||
|
|
||||||
var relayState relay.ProbeResult
|
|
||||||
|
|
||||||
// if the server connection is not established then we will use the general address
|
// if the server connection is not established then we will use the general address
|
||||||
// in case of connection we will use the instance specific address
|
// in case of connection we will use the instance specific address
|
||||||
instanceAddr, err := d.relayMgr.RelayInstanceAddress()
|
instanceAddr, err := d.relayMgr.RelayInstanceAddress()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO add their status
|
// TODO add their status
|
||||||
if errors.Is(err, relayClient.ErrRelayClientNotConnected) {
|
for _, r := range d.relayMgr.ServerURLs() {
|
||||||
for _, r := range d.relayMgr.ServerURLs() {
|
relayStates = append(relayStates, relay.ProbeResult{
|
||||||
relayStates = append(relayStates, relay.ProbeResult{
|
URI: r,
|
||||||
URI: r,
|
Err: err,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
return relayStates
|
|
||||||
}
|
}
|
||||||
relayState.Err = err
|
return relayStates
|
||||||
}
|
}
|
||||||
|
|
||||||
relayState.URI = instanceAddr
|
relayState := relay.ProbeResult{
|
||||||
|
URI: instanceAddr,
|
||||||
|
}
|
||||||
return append(relayStates, relayState)
|
return append(relayStates, relayState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,7 +718,7 @@ func (d *Status) GetDNSStates() []NSGroupState {
|
|||||||
return d.nsGroupStates
|
return d.nsGroupStates
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetResolvedDomainsStates() map[domain.Domain][]netip.Prefix {
|
func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.Unlock()
|
||||||
return maps.Clone(d.resolvedDomainsStates)
|
return maps.Clone(d.resolvedDomainsStates)
|
||||||
@@ -755,6 +776,17 @@ func (d *Status) onConnectionChanged() {
|
|||||||
d.notifier.updateServerStates(d.managementState, d.signalState)
|
d.notifier.updateServerStates(d.managementState, d.signalState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// notifyPeerStateChangeListeners notifies route manager about the change in peer state
|
||||||
|
func (d *Status) notifyPeerStateChangeListeners(peerID string) {
|
||||||
|
ch, found := d.changeNotify[peerID]
|
||||||
|
if !found {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
close(ch)
|
||||||
|
delete(d.changeNotify, peerID)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Status) notifyPeerListChanged() {
|
func (d *Status) notifyPeerListChanged() {
|
||||||
d.notifier.peerListChanged(d.numOfPeers())
|
d.notifier.peerListChanged(d.numOfPeers())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func TestGetPeerStateChangeNotifierLogic(t *testing.T) {
|
|||||||
|
|
||||||
peerState.IP = ip
|
peerState.IP = ip
|
||||||
|
|
||||||
err := status.UpdatePeerState(peerState)
|
err := status.UpdatePeerRelayedStateToDisconnected(peerState)
|
||||||
assert.NoError(t, err, "shouldn't return error")
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ type WorkerICE struct {
|
|||||||
hasRelayOnLocally bool
|
hasRelayOnLocally bool
|
||||||
conn WorkerICECallbacks
|
conn WorkerICECallbacks
|
||||||
|
|
||||||
selectedPriority ConnPriority
|
|
||||||
|
|
||||||
agent *ice.Agent
|
agent *ice.Agent
|
||||||
muxAgent sync.Mutex
|
muxAgent sync.Mutex
|
||||||
|
|
||||||
@@ -57,6 +55,9 @@ type WorkerICE struct {
|
|||||||
|
|
||||||
localUfrag string
|
localUfrag string
|
||||||
localPwd string
|
localPwd string
|
||||||
|
|
||||||
|
// we record the last known state of the ICE agent to avoid duplicate on disconnected events
|
||||||
|
lastKnownState ice.ConnectionState
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWorkerICE(ctx context.Context, log *log.Entry, config ConnConfig, signaler *Signaler, ifaceDiscover stdnet.ExternalIFaceDiscover, statusRecorder *Status, hasRelayOnLocally bool, callBacks WorkerICECallbacks) (*WorkerICE, error) {
|
func NewWorkerICE(ctx context.Context, log *log.Entry, config ConnConfig, signaler *Signaler, ifaceDiscover stdnet.ExternalIFaceDiscover, statusRecorder *Status, hasRelayOnLocally bool, callBacks WorkerICECallbacks) (*WorkerICE, error) {
|
||||||
@@ -92,10 +93,8 @@ func (w *WorkerICE) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
|||||||
|
|
||||||
var preferredCandidateTypes []ice.CandidateType
|
var preferredCandidateTypes []ice.CandidateType
|
||||||
if w.hasRelayOnLocally && remoteOfferAnswer.RelaySrvAddress != "" {
|
if w.hasRelayOnLocally && remoteOfferAnswer.RelaySrvAddress != "" {
|
||||||
w.selectedPriority = connPriorityICEP2P
|
|
||||||
preferredCandidateTypes = icemaker.CandidateTypesP2P()
|
preferredCandidateTypes = icemaker.CandidateTypesP2P()
|
||||||
} else {
|
} else {
|
||||||
w.selectedPriority = connPriorityICETurn
|
|
||||||
preferredCandidateTypes = icemaker.CandidateTypes()
|
preferredCandidateTypes = icemaker.CandidateTypes()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +155,7 @@ func (w *WorkerICE) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
|||||||
RelayedOnLocal: isRelayCandidate(pair.Local),
|
RelayedOnLocal: isRelayCandidate(pair.Local),
|
||||||
}
|
}
|
||||||
w.log.Debugf("on ICE conn read to use ready")
|
w.log.Debugf("on ICE conn read to use ready")
|
||||||
go w.conn.OnConnReady(w.selectedPriority, ci)
|
go w.conn.OnConnReady(selectedPriority(pair), ci)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer.
|
// OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer.
|
||||||
@@ -194,8 +193,7 @@ func (w *WorkerICE) Close() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := w.agent.Close()
|
if err := w.agent.Close(); err != nil {
|
||||||
if err != nil {
|
|
||||||
w.log.Warnf("failed to close ICE agent: %s", err)
|
w.log.Warnf("failed to close ICE agent: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,15 +213,18 @@ func (w *WorkerICE) reCreateAgent(agentCancel context.CancelFunc, candidates []i
|
|||||||
|
|
||||||
err = agent.OnConnectionStateChange(func(state ice.ConnectionState) {
|
err = agent.OnConnectionStateChange(func(state ice.ConnectionState) {
|
||||||
w.log.Debugf("ICE ConnectionState has changed to %s", state.String())
|
w.log.Debugf("ICE ConnectionState has changed to %s", state.String())
|
||||||
if state == ice.ConnectionStateFailed || state == ice.ConnectionStateDisconnected {
|
switch state {
|
||||||
w.conn.OnStatusChanged(StatusDisconnected)
|
case ice.ConnectionStateConnected:
|
||||||
|
w.lastKnownState = ice.ConnectionStateConnected
|
||||||
w.muxAgent.Lock()
|
return
|
||||||
agentCancel()
|
case ice.ConnectionStateFailed, ice.ConnectionStateDisconnected:
|
||||||
_ = agent.Close()
|
if w.lastKnownState != ice.ConnectionStateDisconnected {
|
||||||
w.agent = nil
|
w.lastKnownState = ice.ConnectionStateDisconnected
|
||||||
|
w.conn.OnStatusChanged(StatusDisconnected)
|
||||||
w.muxAgent.Unlock()
|
}
|
||||||
|
w.closeAgent(agentCancel)
|
||||||
|
default:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -249,10 +250,27 @@ func (w *WorkerICE) reCreateAgent(agentCancel context.CancelFunc, candidates []i
|
|||||||
return agent, nil
|
return agent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *WorkerICE) closeAgent(cancel context.CancelFunc) {
|
||||||
|
w.muxAgent.Lock()
|
||||||
|
defer w.muxAgent.Unlock()
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
if err := w.agent.Close(); err != nil {
|
||||||
|
w.log.Warnf("failed to close ICE agent: %s", err)
|
||||||
|
}
|
||||||
|
w.agent = nil
|
||||||
|
}
|
||||||
|
|
||||||
func (w *WorkerICE) punchRemoteWGPort(pair *ice.CandidatePair, remoteWgPort int) {
|
func (w *WorkerICE) punchRemoteWGPort(pair *ice.CandidatePair, remoteWgPort int) {
|
||||||
// wait local endpoint configuration
|
// wait local endpoint configuration
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", pair.Remote.Address(), remoteWgPort))
|
addrString := pair.Remote.Address()
|
||||||
|
parsed, err := netip.ParseAddr(addrString)
|
||||||
|
if (err == nil) && (parsed.Is6()) {
|
||||||
|
addrString = fmt.Sprintf("[%s]", addrString)
|
||||||
|
//IPv6 Literals need to be wrapped in brackets for Resolve*Addr()
|
||||||
|
}
|
||||||
|
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", addrString, remoteWgPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.log.Warnf("got an error while resolving the udp address, err: %s", err)
|
w.log.Warnf("got an error while resolving the udp address, err: %s", err)
|
||||||
return
|
return
|
||||||
@@ -378,3 +396,11 @@ func isRelayed(pair *ice.CandidatePair) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selectedPriority(pair *ice.CandidatePair) ConnPriority {
|
||||||
|
if isRelayed(pair) {
|
||||||
|
return connPriorityICETurn
|
||||||
|
} else {
|
||||||
|
return connPriorityICEP2P
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
87
client/internal/peerstore/store.go
Normal file
87
client/internal/peerstore/store.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package peerstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store is a thread-safe store for peer connections.
|
||||||
|
type Store struct {
|
||||||
|
peerConns map[string]*peer.Conn
|
||||||
|
peerConnsMu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnStore() *Store {
|
||||||
|
return &Store{
|
||||||
|
peerConns: make(map[string]*peer.Conn),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) AddPeerConn(pubKey string, conn *peer.Conn) bool {
|
||||||
|
s.peerConnsMu.Lock()
|
||||||
|
defer s.peerConnsMu.Unlock()
|
||||||
|
|
||||||
|
_, ok := s.peerConns[pubKey]
|
||||||
|
if ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
s.peerConns[pubKey] = conn
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Remove(pubKey string) (*peer.Conn, bool) {
|
||||||
|
s.peerConnsMu.Lock()
|
||||||
|
defer s.peerConnsMu.Unlock()
|
||||||
|
|
||||||
|
p, ok := s.peerConns[pubKey]
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
delete(s.peerConns, pubKey)
|
||||||
|
return p, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) AllowedIPs(pubKey string) (string, bool) {
|
||||||
|
s.peerConnsMu.RLock()
|
||||||
|
defer s.peerConnsMu.RUnlock()
|
||||||
|
|
||||||
|
p, ok := s.peerConns[pubKey]
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return p.WgConfig().AllowedIps, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) AllowedIP(pubKey string) (net.IP, bool) {
|
||||||
|
s.peerConnsMu.RLock()
|
||||||
|
defer s.peerConnsMu.RUnlock()
|
||||||
|
|
||||||
|
p, ok := s.peerConns[pubKey]
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return p.AllowedIP(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) PeerConn(pubKey string) (*peer.Conn, bool) {
|
||||||
|
s.peerConnsMu.RLock()
|
||||||
|
defer s.peerConnsMu.RUnlock()
|
||||||
|
|
||||||
|
p, ok := s.peerConns[pubKey]
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return p, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) PeersPubKey() []string {
|
||||||
|
s.peerConnsMu.RLock()
|
||||||
|
defer s.peerConnsMu.RUnlock()
|
||||||
|
|
||||||
|
return maps.Keys(s.peerConns)
|
||||||
|
}
|
||||||
@@ -13,12 +13,20 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
nbdns "github.com/netbirdio/netbird/client/internal/dns"
|
nbdns "github.com/netbirdio/netbird/client/internal/dns"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peerstore"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/dnsinterceptor"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
|
"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/static"
|
"github.com/netbirdio/netbird/client/internal/routemanager/static"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
handlerTypeDynamic = iota
|
||||||
|
handlerTypeDomain
|
||||||
|
handlerTypeStatic
|
||||||
|
)
|
||||||
|
|
||||||
type routerPeerStatus struct {
|
type routerPeerStatus struct {
|
||||||
connected bool
|
connected bool
|
||||||
relayed bool
|
relayed bool
|
||||||
@@ -53,7 +61,18 @@ type clientNetwork struct {
|
|||||||
updateSerial uint64
|
updateSerial uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func newClientNetworkWatcher(ctx context.Context, dnsRouteInterval time.Duration, wgInterface iface.IWGIface, statusRecorder *peer.Status, rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allowedIPsRefCounter *refcounter.AllowedIPsRefCounter) *clientNetwork {
|
func newClientNetworkWatcher(
|
||||||
|
ctx context.Context,
|
||||||
|
dnsRouteInterval time.Duration,
|
||||||
|
wgInterface iface.IWGIface,
|
||||||
|
statusRecorder *peer.Status,
|
||||||
|
rt *route.Route,
|
||||||
|
routeRefCounter *refcounter.RouteRefCounter,
|
||||||
|
allowedIPsRefCounter *refcounter.AllowedIPsRefCounter,
|
||||||
|
dnsServer nbdns.Server,
|
||||||
|
peerStore *peerstore.Store,
|
||||||
|
useNewDNSRoute bool,
|
||||||
|
) *clientNetwork {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
client := &clientNetwork{
|
client := &clientNetwork{
|
||||||
@@ -65,7 +84,17 @@ func newClientNetworkWatcher(ctx context.Context, dnsRouteInterval time.Duration
|
|||||||
routePeersNotifiers: make(map[string]chan struct{}),
|
routePeersNotifiers: make(map[string]chan struct{}),
|
||||||
routeUpdate: make(chan routesUpdate),
|
routeUpdate: make(chan routesUpdate),
|
||||||
peerStateUpdate: make(chan struct{}),
|
peerStateUpdate: make(chan struct{}),
|
||||||
handler: handlerFromRoute(rt, routeRefCounter, allowedIPsRefCounter, dnsRouteInterval, statusRecorder, wgInterface),
|
handler: handlerFromRoute(
|
||||||
|
rt,
|
||||||
|
routeRefCounter,
|
||||||
|
allowedIPsRefCounter,
|
||||||
|
dnsRouteInterval,
|
||||||
|
statusRecorder,
|
||||||
|
wgInterface,
|
||||||
|
dnsServer,
|
||||||
|
peerStore,
|
||||||
|
useNewDNSRoute,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
@@ -122,13 +151,20 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID]
|
|||||||
tempScore = float64(metricDiff) * 10
|
tempScore = float64(metricDiff) * 10
|
||||||
}
|
}
|
||||||
|
|
||||||
// in some temporal cases, latency can be 0, so we set it to 1s to not block but try to avoid this route
|
// in some temporal cases, latency can be 0, so we set it to 999ms to not block but try to avoid this route
|
||||||
latency := time.Second
|
latency := 999 * time.Millisecond
|
||||||
if peerStatus.latency != 0 {
|
if peerStatus.latency != 0 {
|
||||||
latency = peerStatus.latency
|
latency = peerStatus.latency
|
||||||
} else {
|
} else {
|
||||||
log.Warnf("peer %s has 0 latency", r.Peer)
|
log.Tracef("peer %s has 0 latency, range %s", r.Peer, c.handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// avoid negative tempScore on the higher latency calculation
|
||||||
|
if latency > 1*time.Second {
|
||||||
|
latency = 999 * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
// higher latency is worse score
|
||||||
tempScore += 1 - latency.Seconds()
|
tempScore += 1 - latency.Seconds()
|
||||||
|
|
||||||
if !peerStatus.relayed {
|
if !peerStatus.relayed {
|
||||||
@@ -150,6 +186,8 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debugf("chosen route: %s, chosen score: %f, current route: %s, current score: %f", chosen, chosenScore, currID, currScore)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case chosen == "":
|
case chosen == "":
|
||||||
var peers []string
|
var peers []string
|
||||||
@@ -195,15 +233,20 @@ func (c *clientNetwork) watchPeerStatusChanges(ctx context.Context, peerKey stri
|
|||||||
func (c *clientNetwork) startPeersStatusChangeWatcher() {
|
func (c *clientNetwork) startPeersStatusChangeWatcher() {
|
||||||
for _, r := range c.routes {
|
for _, r := range c.routes {
|
||||||
_, found := c.routePeersNotifiers[r.Peer]
|
_, found := c.routePeersNotifiers[r.Peer]
|
||||||
if !found {
|
if found {
|
||||||
c.routePeersNotifiers[r.Peer] = make(chan struct{})
|
continue
|
||||||
go c.watchPeerStatusChanges(c.ctx, r.Peer, c.peerStateUpdate, c.routePeersNotifiers[r.Peer])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closerChan := make(chan struct{})
|
||||||
|
c.routePeersNotifiers[r.Peer] = closerChan
|
||||||
|
go c.watchPeerStatusChanges(c.ctx, r.Peer, c.peerStateUpdate, closerChan)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *clientNetwork) removeRouteFromWireguardPeer() error {
|
func (c *clientNetwork) removeRouteFromWireGuardPeer() error {
|
||||||
c.removeStateRoute()
|
if err := c.statusRecorder.RemovePeerStateRoute(c.currentChosen.Peer, c.handler.String()); err != nil {
|
||||||
|
log.Warnf("Failed to update peer state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.handler.RemoveAllowedIPs(); err != nil {
|
if err := c.handler.RemoveAllowedIPs(); err != nil {
|
||||||
return fmt.Errorf("remove allowed IPs: %w", err)
|
return fmt.Errorf("remove allowed IPs: %w", err)
|
||||||
@@ -218,7 +261,7 @@ func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
|
|||||||
|
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
|
|
||||||
if err := c.removeRouteFromWireguardPeer(); err != nil {
|
if err := c.removeRouteFromWireGuardPeer(); err != nil {
|
||||||
merr = multierror.Append(merr, fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err))
|
merr = multierror.Append(merr, fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err))
|
||||||
}
|
}
|
||||||
if err := c.handler.RemoveRoute(); err != nil {
|
if err := c.handler.RemoveRoute(); err != nil {
|
||||||
@@ -257,7 +300,7 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, remove the allowed IPs from the previous peer first
|
// Otherwise, remove the allowed IPs from the previous peer first
|
||||||
if err := c.removeRouteFromWireguardPeer(); err != nil {
|
if err := c.removeRouteFromWireGuardPeer(); err != nil {
|
||||||
return fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err)
|
return fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,37 +311,13 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
|
|||||||
return fmt.Errorf("add allowed IPs for peer %s: %w", c.currentChosen.Peer, err)
|
return fmt.Errorf("add allowed IPs for peer %s: %w", c.currentChosen.Peer, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.addStateRoute()
|
err := c.statusRecorder.AddPeerStateRoute(c.currentChosen.Peer, c.handler.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add peer state route: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *clientNetwork) addStateRoute() {
|
|
||||||
state, err := c.statusRecorder.GetPeer(c.currentChosen.Peer)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to get peer state: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state.AddRoute(c.handler.String())
|
|
||||||
if err := c.statusRecorder.UpdatePeerState(state); err != nil {
|
|
||||||
log.Warnf("Failed to update peer state: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *clientNetwork) removeStateRoute() {
|
|
||||||
state, err := c.statusRecorder.GetPeer(c.currentChosen.Peer)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to get peer state: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state.DeleteRoute(c.handler.String())
|
|
||||||
if err := c.statusRecorder.UpdatePeerState(state); err != nil {
|
|
||||||
log.Warnf("Failed to update peer state: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) {
|
func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) {
|
||||||
go func() {
|
go func() {
|
||||||
c.routeUpdate <- update
|
c.routeUpdate <- update
|
||||||
@@ -378,10 +397,50 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlerFromRoute(rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, dnsRouterInteval time.Duration, statusRecorder *peer.Status, wgInterface iface.IWGIface) RouteHandler {
|
func handlerFromRoute(
|
||||||
if rt.IsDynamic() {
|
rt *route.Route,
|
||||||
|
routeRefCounter *refcounter.RouteRefCounter,
|
||||||
|
allowedIPsRefCounter *refcounter.AllowedIPsRefCounter,
|
||||||
|
dnsRouterInteval time.Duration,
|
||||||
|
statusRecorder *peer.Status,
|
||||||
|
wgInterface iface.IWGIface,
|
||||||
|
dnsServer nbdns.Server,
|
||||||
|
peerStore *peerstore.Store,
|
||||||
|
useNewDNSRoute bool,
|
||||||
|
) RouteHandler {
|
||||||
|
switch handlerType(rt, useNewDNSRoute) {
|
||||||
|
case handlerTypeDomain:
|
||||||
|
return dnsinterceptor.New(
|
||||||
|
rt,
|
||||||
|
routeRefCounter,
|
||||||
|
allowedIPsRefCounter,
|
||||||
|
statusRecorder,
|
||||||
|
dnsServer,
|
||||||
|
peerStore,
|
||||||
|
)
|
||||||
|
case handlerTypeDynamic:
|
||||||
dns := nbdns.NewServiceViaMemory(wgInterface)
|
dns := nbdns.NewServiceViaMemory(wgInterface)
|
||||||
return dynamic.NewRoute(rt, routeRefCounter, allowedIPsRefCounter, dnsRouterInteval, statusRecorder, wgInterface, fmt.Sprintf("%s:%d", dns.RuntimeIP(), dns.RuntimePort()))
|
return dynamic.NewRoute(
|
||||||
|
rt,
|
||||||
|
routeRefCounter,
|
||||||
|
allowedIPsRefCounter,
|
||||||
|
dnsRouterInteval,
|
||||||
|
statusRecorder,
|
||||||
|
wgInterface,
|
||||||
|
fmt.Sprintf("%s:%d", dns.RuntimeIP(), dns.RuntimePort()),
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return static.NewRoute(rt, routeRefCounter, allowedIPsRefCounter)
|
||||||
}
|
}
|
||||||
return static.NewRoute(rt, routeRefCounter, allowedIPsRefCounter)
|
}
|
||||||
|
|
||||||
|
func handlerType(rt *route.Route, useNewDNSRoute bool) int {
|
||||||
|
if !rt.IsDynamic() {
|
||||||
|
return handlerTypeStatic
|
||||||
|
}
|
||||||
|
|
||||||
|
if useNewDNSRoute {
|
||||||
|
return handlerTypeDomain
|
||||||
|
}
|
||||||
|
return handlerTypeDynamic
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package routemanager
|
package routemanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -227,6 +228,64 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
currentRoute: "route1",
|
currentRoute: "route1",
|
||||||
expectedRouteID: "route1",
|
expectedRouteID: "route1",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "relayed routes with latency 0 should maintain previous choice",
|
||||||
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
|
"route1": {
|
||||||
|
connected: true,
|
||||||
|
relayed: true,
|
||||||
|
latency: 0 * time.Millisecond,
|
||||||
|
},
|
||||||
|
"route2": {
|
||||||
|
connected: true,
|
||||||
|
relayed: true,
|
||||||
|
latency: 0 * time.Millisecond,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
|
"route1": {
|
||||||
|
ID: "route1",
|
||||||
|
Metric: route.MaxMetric,
|
||||||
|
Peer: "peer1",
|
||||||
|
},
|
||||||
|
"route2": {
|
||||||
|
ID: "route2",
|
||||||
|
Metric: route.MaxMetric,
|
||||||
|
Peer: "peer2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentRoute: "route1",
|
||||||
|
expectedRouteID: "route1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "p2p routes with latency 0 should maintain previous choice",
|
||||||
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
|
"route1": {
|
||||||
|
connected: true,
|
||||||
|
relayed: false,
|
||||||
|
latency: 0 * time.Millisecond,
|
||||||
|
},
|
||||||
|
"route2": {
|
||||||
|
connected: true,
|
||||||
|
relayed: false,
|
||||||
|
latency: 0 * time.Millisecond,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
|
"route1": {
|
||||||
|
ID: "route1",
|
||||||
|
Metric: route.MaxMetric,
|
||||||
|
Peer: "peer1",
|
||||||
|
},
|
||||||
|
"route2": {
|
||||||
|
ID: "route2",
|
||||||
|
Metric: route.MaxMetric,
|
||||||
|
Peer: "peer2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentRoute: "route1",
|
||||||
|
expectedRouteID: "route1",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "current route with bad score should be changed to route with better score",
|
name: "current route with bad score should be changed to route with better score",
|
||||||
statuses: map[route.ID]routerPeerStatus{
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
@@ -287,6 +346,45 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fill the test data with random routes
|
||||||
|
for _, tc := range testCases {
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
dummyRoute := &route.Route{
|
||||||
|
ID: route.ID(fmt.Sprintf("dummy_p1_%d", i)),
|
||||||
|
Metric: route.MinMetric,
|
||||||
|
Peer: fmt.Sprintf("dummy_p1_%d", i),
|
||||||
|
}
|
||||||
|
tc.existingRoutes[dummyRoute.ID] = dummyRoute
|
||||||
|
}
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
dummyRoute := &route.Route{
|
||||||
|
ID: route.ID(fmt.Sprintf("dummy_p2_%d", i)),
|
||||||
|
Metric: route.MinMetric,
|
||||||
|
Peer: fmt.Sprintf("dummy_p1_%d", i),
|
||||||
|
}
|
||||||
|
tc.existingRoutes[dummyRoute.ID] = dummyRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
id := route.ID(fmt.Sprintf("dummy_p1_%d", i))
|
||||||
|
dummyStatus := routerPeerStatus{
|
||||||
|
connected: false,
|
||||||
|
relayed: true,
|
||||||
|
latency: 0,
|
||||||
|
}
|
||||||
|
tc.statuses[id] = dummyStatus
|
||||||
|
}
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
id := route.ID(fmt.Sprintf("dummy_p2_%d", i))
|
||||||
|
dummyStatus := routerPeerStatus{
|
||||||
|
connected: false,
|
||||||
|
relayed: true,
|
||||||
|
latency: 0,
|
||||||
|
}
|
||||||
|
tc.statuses[id] = dummyStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
currentRoute := &route.Route{
|
currentRoute := &route.Route{
|
||||||
|
|||||||
356
client/internal/routemanager/dnsinterceptor/handler.go
Normal file
356
client/internal/routemanager/dnsinterceptor/handler.go
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
package dnsinterceptor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
nbdns "github.com/netbirdio/netbird/client/internal/dns"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dnsfwd"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peerstore"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
)
|
||||||
|
|
||||||
|
type domainMap map[domain.Domain][]netip.Prefix
|
||||||
|
|
||||||
|
type DnsInterceptor struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
route *route.Route
|
||||||
|
routeRefCounter *refcounter.RouteRefCounter
|
||||||
|
allowedIPsRefcounter *refcounter.AllowedIPsRefCounter
|
||||||
|
statusRecorder *peer.Status
|
||||||
|
dnsServer nbdns.Server
|
||||||
|
currentPeerKey string
|
||||||
|
interceptedDomains domainMap
|
||||||
|
peerStore *peerstore.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(
|
||||||
|
rt *route.Route,
|
||||||
|
routeRefCounter *refcounter.RouteRefCounter,
|
||||||
|
allowedIPsRefCounter *refcounter.AllowedIPsRefCounter,
|
||||||
|
statusRecorder *peer.Status,
|
||||||
|
dnsServer nbdns.Server,
|
||||||
|
peerStore *peerstore.Store,
|
||||||
|
) *DnsInterceptor {
|
||||||
|
return &DnsInterceptor{
|
||||||
|
route: rt,
|
||||||
|
routeRefCounter: routeRefCounter,
|
||||||
|
allowedIPsRefcounter: allowedIPsRefCounter,
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
|
dnsServer: dnsServer,
|
||||||
|
interceptedDomains: make(domainMap),
|
||||||
|
peerStore: peerStore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DnsInterceptor) String() string {
|
||||||
|
return d.route.Domains.SafeString()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DnsInterceptor) AddRoute(context.Context) error {
|
||||||
|
d.dnsServer.RegisterHandler(d.route.Domains.ToPunycodeList(), d, nbdns.PriorityDNSRoute)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DnsInterceptor) RemoveRoute() error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
for domain, prefixes := range d.interceptedDomains {
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if _, err := d.routeRefCounter.Decrement(prefix); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove dynamic route for IP %s: %v", prefix, err))
|
||||||
|
}
|
||||||
|
if d.currentPeerKey != "" {
|
||||||
|
if _, err := d.allowedIPsRefcounter.Decrement(prefix); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove allowed IP %s: %v", prefix, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("removed dynamic route(s) for [%s]: %s", domain.SafeString(), strings.ReplaceAll(fmt.Sprintf("%s", prefixes), " ", ", "))
|
||||||
|
|
||||||
|
}
|
||||||
|
for _, domain := range d.route.Domains {
|
||||||
|
d.statusRecorder.DeleteResolvedDomainsStates(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(d.interceptedDomains)
|
||||||
|
|
||||||
|
d.dnsServer.DeregisterHandler(d.route.Domains.ToPunycodeList(), nbdns.PriorityDNSRoute)
|
||||||
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DnsInterceptor) AddAllowedIPs(peerKey string) error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
for domain, prefixes := range d.interceptedDomains {
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if ref, err := d.allowedIPsRefcounter.Increment(prefix, peerKey); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("add allowed IP %s: %v", prefix, err))
|
||||||
|
} else if ref.Count > 1 && ref.Out != peerKey {
|
||||||
|
log.Warnf("IP [%s] for domain [%s] is already routed by peer [%s]. HA routing disabled",
|
||||||
|
prefix.Addr(),
|
||||||
|
domain.SafeString(),
|
||||||
|
ref.Out,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.currentPeerKey = peerKey
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DnsInterceptor) RemoveAllowedIPs() error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
for _, prefixes := range d.interceptedDomains {
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if _, err := d.allowedIPsRefcounter.Decrement(prefix); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove allowed IP %s: %v", prefix, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.currentPeerKey = ""
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeDNS implements the dns.Handler interface
|
||||||
|
func (d *DnsInterceptor) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
if len(r.Question) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Tracef("received DNS request for domain=%s type=%v class=%v",
|
||||||
|
r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass)
|
||||||
|
|
||||||
|
d.mu.RLock()
|
||||||
|
peerKey := d.currentPeerKey
|
||||||
|
d.mu.RUnlock()
|
||||||
|
|
||||||
|
if peerKey == "" {
|
||||||
|
log.Tracef("no current peer key set, letting next handler try for domain=%s", r.Question[0].Name)
|
||||||
|
|
||||||
|
d.continueToNextHandler(w, r, "no current peer key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
upstreamIP, err := d.getUpstreamIP(peerKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to get upstream IP: %v", err)
|
||||||
|
d.continueToNextHandler(w, r, fmt.Sprintf("failed to get upstream IP: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &dns.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
Net: "udp",
|
||||||
|
}
|
||||||
|
upstream := fmt.Sprintf("%s:%d", upstreamIP, dnsfwd.ListenPort)
|
||||||
|
reply, _, err := client.ExchangeContext(context.Background(), r, upstream)
|
||||||
|
|
||||||
|
var answer []dns.RR
|
||||||
|
if reply != nil {
|
||||||
|
answer = reply.Answer
|
||||||
|
}
|
||||||
|
log.Tracef("upstream %s (%s) DNS response for domain=%s answers=%v", upstreamIP, peerKey, r.Question[0].Name, answer)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to exchange DNS request with %s: %v", upstream, err)
|
||||||
|
if err := w.WriteMsg(&dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeServerFailure, Id: r.Id}}); err != nil {
|
||||||
|
log.Errorf("failed writing DNS response: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.Id = r.Id
|
||||||
|
if err := d.writeMsg(w, reply); err != nil {
|
||||||
|
log.Errorf("failed writing DNS response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// continueToNextHandler signals the handler chain to try the next handler
|
||||||
|
func (d *DnsInterceptor) continueToNextHandler(w dns.ResponseWriter, r *dns.Msg, reason string) {
|
||||||
|
log.Tracef("continuing to next handler for domain=%s reason=%s", r.Question[0].Name, reason)
|
||||||
|
|
||||||
|
resp := new(dns.Msg)
|
||||||
|
resp.SetRcode(r, dns.RcodeNameError)
|
||||||
|
// Set Zero bit to signal handler chain to continue
|
||||||
|
resp.MsgHdr.Zero = true
|
||||||
|
if err := w.WriteMsg(resp); err != nil {
|
||||||
|
log.Errorf("failed writing DNS continue response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DnsInterceptor) getUpstreamIP(peerKey string) (net.IP, error) {
|
||||||
|
peerAllowedIP, exists := d.peerStore.AllowedIP(peerKey)
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("peer connection not found for key: %s", peerKey)
|
||||||
|
}
|
||||||
|
return peerAllowedIP, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DnsInterceptor) writeMsg(w dns.ResponseWriter, r *dns.Msg) error {
|
||||||
|
if r == nil {
|
||||||
|
return fmt.Errorf("received nil DNS message")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.Answer) > 0 && len(r.Question) > 0 {
|
||||||
|
origPattern := ""
|
||||||
|
if writer, ok := w.(*nbdns.ResponseWriterChain); ok {
|
||||||
|
origPattern = writer.GetOrigPattern()
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedDomain := domain.Domain(r.Question[0].Name)
|
||||||
|
|
||||||
|
// already punycode via RegisterHandler()
|
||||||
|
originalDomain := domain.Domain(origPattern)
|
||||||
|
if originalDomain == "" {
|
||||||
|
originalDomain = resolvedDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPrefixes []netip.Prefix
|
||||||
|
for _, answer := range r.Answer {
|
||||||
|
var ip netip.Addr
|
||||||
|
switch rr := answer.(type) {
|
||||||
|
case *dns.A:
|
||||||
|
addr, ok := netip.AddrFromSlice(rr.A)
|
||||||
|
if !ok {
|
||||||
|
log.Tracef("failed to convert A record for domain=%s ip=%v", resolvedDomain, rr.A)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip = addr
|
||||||
|
case *dns.AAAA:
|
||||||
|
addr, ok := netip.AddrFromSlice(rr.AAAA)
|
||||||
|
if !ok {
|
||||||
|
log.Tracef("failed to convert AAAA record for domain=%s ip=%v", resolvedDomain, rr.AAAA)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip = addr
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := netip.PrefixFrom(ip, ip.BitLen())
|
||||||
|
newPrefixes = append(newPrefixes, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newPrefixes) > 0 {
|
||||||
|
if err := d.updateDomainPrefixes(resolvedDomain, originalDomain, newPrefixes); err != nil {
|
||||||
|
log.Errorf("failed to update domain prefixes: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.WriteMsg(r); err != nil {
|
||||||
|
return fmt.Errorf("failed to write DNS response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DnsInterceptor) updateDomainPrefixes(resolvedDomain, originalDomain domain.Domain, newPrefixes []netip.Prefix) error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
oldPrefixes := d.interceptedDomains[resolvedDomain]
|
||||||
|
toAdd, toRemove := determinePrefixChanges(oldPrefixes, newPrefixes)
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
|
||||||
|
// Add new prefixes
|
||||||
|
for _, prefix := range toAdd {
|
||||||
|
if _, err := d.routeRefCounter.Increment(prefix, struct{}{}); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("add route for IP %s: %v", prefix, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.currentPeerKey == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ref, err := d.allowedIPsRefcounter.Increment(prefix, d.currentPeerKey); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("add allowed IP %s: %v", prefix, err))
|
||||||
|
} else if ref.Count > 1 && ref.Out != d.currentPeerKey {
|
||||||
|
log.Warnf("IP [%s] for domain [%s] is already routed by peer [%s]. HA routing disabled",
|
||||||
|
prefix.Addr(),
|
||||||
|
resolvedDomain.SafeString(),
|
||||||
|
ref.Out,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !d.route.KeepRoute {
|
||||||
|
// Remove old prefixes
|
||||||
|
for _, prefix := range toRemove {
|
||||||
|
if _, err := d.routeRefCounter.Decrement(prefix); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove route for IP %s: %v", prefix, err))
|
||||||
|
}
|
||||||
|
if d.currentPeerKey != "" {
|
||||||
|
if _, err := d.allowedIPsRefcounter.Decrement(prefix); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove allowed IP %s: %v", prefix, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update domain prefixes using resolved domain as key
|
||||||
|
if len(toAdd) > 0 || len(toRemove) > 0 {
|
||||||
|
d.interceptedDomains[resolvedDomain] = newPrefixes
|
||||||
|
originalDomain = domain.Domain(strings.TrimSuffix(string(originalDomain), "."))
|
||||||
|
d.statusRecorder.UpdateResolvedDomainsStates(originalDomain, resolvedDomain, newPrefixes)
|
||||||
|
|
||||||
|
if len(toAdd) > 0 {
|
||||||
|
log.Debugf("added dynamic route(s) for domain=%s (pattern: domain=%s): %s",
|
||||||
|
resolvedDomain.SafeString(),
|
||||||
|
originalDomain.SafeString(),
|
||||||
|
toAdd)
|
||||||
|
}
|
||||||
|
if len(toRemove) > 0 {
|
||||||
|
log.Debugf("removed dynamic route(s) for domain=%s (pattern: domain=%s): %s",
|
||||||
|
resolvedDomain.SafeString(),
|
||||||
|
originalDomain.SafeString(),
|
||||||
|
toRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func determinePrefixChanges(oldPrefixes, newPrefixes []netip.Prefix) (toAdd, toRemove []netip.Prefix) {
|
||||||
|
prefixSet := make(map[netip.Prefix]bool)
|
||||||
|
for _, prefix := range oldPrefixes {
|
||||||
|
prefixSet[prefix] = false
|
||||||
|
}
|
||||||
|
for _, prefix := range newPrefixes {
|
||||||
|
if _, exists := prefixSet[prefix]; exists {
|
||||||
|
prefixSet[prefix] = true
|
||||||
|
} else {
|
||||||
|
toAdd = append(toAdd, prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for prefix, inUse := range prefixSet {
|
||||||
|
if !inUse {
|
||||||
|
toRemove = append(toRemove, prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -74,11 +74,7 @@ func NewRoute(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Route) String() string {
|
func (r *Route) String() string {
|
||||||
s, err := r.route.Domains.String()
|
return r.route.Domains.SafeString()
|
||||||
if err != nil {
|
|
||||||
return r.route.Domains.PunycodeString()
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Route) AddRoute(ctx context.Context) error {
|
func (r *Route) AddRoute(ctx context.Context) error {
|
||||||
@@ -292,7 +288,7 @@ func (r *Route) updateDynamicRoutes(ctx context.Context, newDomains domainMap) e
|
|||||||
updatedPrefixes := combinePrefixes(oldPrefixes, removedPrefixes, addedPrefixes)
|
updatedPrefixes := combinePrefixes(oldPrefixes, removedPrefixes, addedPrefixes)
|
||||||
r.dynamicDomains[domain] = updatedPrefixes
|
r.dynamicDomains[domain] = updatedPrefixes
|
||||||
|
|
||||||
r.statusRecorder.UpdateResolvedDomainsStates(domain, updatedPrefixes)
|
r.statusRecorder.UpdateResolvedDomainsStates(domain, domain, updatedPrefixes)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
|||||||
@@ -12,12 +12,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "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/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns"
|
||||||
"github.com/netbirdio/netbird/client/internal/listener"
|
"github.com/netbirdio/netbird/client/internal/listener"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peerstore"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/notifier"
|
"github.com/netbirdio/netbird/client/internal/routemanager/notifier"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
@@ -32,10 +35,12 @@ import (
|
|||||||
|
|
||||||
// Manager is a route manager interface
|
// Manager is a route manager interface
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
Init(*statemanager.Manager) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error)
|
Init() (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error)
|
||||||
UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, error)
|
UpdateRoutes(updateSerial uint64, newRoutes []*route.Route, useNewDNSRoute bool) error
|
||||||
TriggerSelection(route.HAMap)
|
TriggerSelection(route.HAMap)
|
||||||
GetRouteSelector() *routeselector.RouteSelector
|
GetRouteSelector() *routeselector.RouteSelector
|
||||||
|
GetClientRoutes() route.HAMap
|
||||||
|
GetClientRoutesWithNetID() map[route.NetID][]*route.Route
|
||||||
SetRouteChangeListener(listener listener.NetworkChangeListener)
|
SetRouteChangeListener(listener listener.NetworkChangeListener)
|
||||||
InitialRouteRange() []string
|
InitialRouteRange() []string
|
||||||
EnableServerRouter(firewall firewall.Manager) error
|
EnableServerRouter(firewall firewall.Manager) error
|
||||||
@@ -59,6 +64,12 @@ type DefaultManager struct {
|
|||||||
routeRefCounter *refcounter.RouteRefCounter
|
routeRefCounter *refcounter.RouteRefCounter
|
||||||
allowedIPsRefCounter *refcounter.AllowedIPsRefCounter
|
allowedIPsRefCounter *refcounter.AllowedIPsRefCounter
|
||||||
dnsRouteInterval time.Duration
|
dnsRouteInterval time.Duration
|
||||||
|
stateManager *statemanager.Manager
|
||||||
|
// clientRoutes is the most recent list of clientRoutes received from the Management Service
|
||||||
|
clientRoutes route.HAMap
|
||||||
|
dnsServer dns.Server
|
||||||
|
peerStore *peerstore.Store
|
||||||
|
useNewDNSRoute bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(
|
func NewManager(
|
||||||
@@ -69,6 +80,9 @@ func NewManager(
|
|||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
relayMgr *relayClient.Manager,
|
relayMgr *relayClient.Manager,
|
||||||
initialRoutes []*route.Route,
|
initialRoutes []*route.Route,
|
||||||
|
stateManager *statemanager.Manager,
|
||||||
|
dnsServer dns.Server,
|
||||||
|
peerStore *peerstore.Store,
|
||||||
) *DefaultManager {
|
) *DefaultManager {
|
||||||
mCTX, cancel := context.WithCancel(ctx)
|
mCTX, cancel := context.WithCancel(ctx)
|
||||||
notifier := notifier.NewNotifier()
|
notifier := notifier.NewNotifier()
|
||||||
@@ -80,12 +94,14 @@ func NewManager(
|
|||||||
dnsRouteInterval: dnsRouteInterval,
|
dnsRouteInterval: dnsRouteInterval,
|
||||||
clientNetworks: make(map[route.HAUniqueID]*clientNetwork),
|
clientNetworks: make(map[route.HAUniqueID]*clientNetwork),
|
||||||
relayMgr: relayMgr,
|
relayMgr: relayMgr,
|
||||||
routeSelector: routeselector.NewRouteSelector(),
|
|
||||||
sysOps: sysOps,
|
sysOps: sysOps,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
wgInterface: wgInterface,
|
wgInterface: wgInterface,
|
||||||
pubKey: pubKey,
|
pubKey: pubKey,
|
||||||
notifier: notifier,
|
notifier: notifier,
|
||||||
|
stateManager: stateManager,
|
||||||
|
dnsServer: dnsServer,
|
||||||
|
peerStore: peerStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
dm.routeRefCounter = refcounter.New(
|
dm.routeRefCounter = refcounter.New(
|
||||||
@@ -114,14 +130,16 @@ func NewManager(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if runtime.GOOS == "android" {
|
if runtime.GOOS == "android" {
|
||||||
cr := dm.clientRoutes(initialRoutes)
|
cr := dm.initialClientRoutes(initialRoutes)
|
||||||
dm.notifier.SetInitialClientRoutes(cr)
|
dm.notifier.SetInitialClientRoutes(cr)
|
||||||
}
|
}
|
||||||
return dm
|
return dm
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init sets up the routing
|
// Init sets up the routing
|
||||||
func (m *DefaultManager) Init(stateManager *statemanager.Manager) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) {
|
func (m *DefaultManager) Init() (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) {
|
||||||
|
m.routeSelector = m.initSelector()
|
||||||
|
|
||||||
if nbnet.CustomRoutingDisabled() {
|
if nbnet.CustomRoutingDisabled() {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
@@ -137,14 +155,36 @@ func (m *DefaultManager) Init(stateManager *statemanager.Manager) (nbnet.AddHook
|
|||||||
|
|
||||||
ips := resolveURLsToIPs(initialAddresses)
|
ips := resolveURLsToIPs(initialAddresses)
|
||||||
|
|
||||||
beforePeerHook, afterPeerHook, err := m.sysOps.SetupRouting(ips, stateManager)
|
beforePeerHook, afterPeerHook, err := m.sysOps.SetupRouting(ips, m.stateManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("setup routing: %w", err)
|
return nil, nil, fmt.Errorf("setup routing: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Routing setup complete")
|
log.Info("Routing setup complete")
|
||||||
return beforePeerHook, afterPeerHook, nil
|
return beforePeerHook, afterPeerHook, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *DefaultManager) initSelector() *routeselector.RouteSelector {
|
||||||
|
var state *SelectorState
|
||||||
|
m.stateManager.RegisterState(state)
|
||||||
|
|
||||||
|
// restore selector state if it exists
|
||||||
|
if err := m.stateManager.LoadState(state); err != nil {
|
||||||
|
log.Warnf("failed to load state: %v", err)
|
||||||
|
return routeselector.NewRouteSelector()
|
||||||
|
}
|
||||||
|
|
||||||
|
if state := m.stateManager.GetState(state); state != nil {
|
||||||
|
if selector, ok := state.(*SelectorState); ok {
|
||||||
|
return (*routeselector.RouteSelector)(selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("failed to convert state with type %T to SelectorState", state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return routeselector.NewRouteSelector()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *DefaultManager) EnableServerRouter(firewall firewall.Manager) error {
|
func (m *DefaultManager) EnableServerRouter(firewall firewall.Manager) error {
|
||||||
var err error
|
var err error
|
||||||
m.serverRouter, err = newServerRouter(m.ctx, m.wgInterface, firewall, m.statusRecorder)
|
m.serverRouter, err = newServerRouter(m.ctx, m.wgInterface, firewall, m.statusRecorder)
|
||||||
@@ -181,33 +221,41 @@ func (m *DefaultManager) Stop(stateManager *statemanager.Manager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.ctx = nil
|
m.ctx = nil
|
||||||
|
|
||||||
|
m.mux.Lock()
|
||||||
|
defer m.mux.Unlock()
|
||||||
|
m.clientRoutes = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRoutes compares received routes with existing routes and removes, updates or adds them to the client and server maps
|
// UpdateRoutes compares received routes with existing routes and removes, updates or adds them to the client and server maps
|
||||||
func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, error) {
|
func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route, useNewDNSRoute bool) error {
|
||||||
select {
|
select {
|
||||||
case <-m.ctx.Done():
|
case <-m.ctx.Done():
|
||||||
log.Infof("not updating routes as context is closed")
|
log.Infof("not updating routes as context is closed")
|
||||||
return nil, nil, m.ctx.Err()
|
return nil
|
||||||
default:
|
default:
|
||||||
m.mux.Lock()
|
|
||||||
defer m.mux.Unlock()
|
|
||||||
|
|
||||||
newServerRoutesMap, newClientRoutesIDMap := m.classifyRoutes(newRoutes)
|
|
||||||
|
|
||||||
filteredClientRoutes := m.routeSelector.FilterSelected(newClientRoutesIDMap)
|
|
||||||
m.updateClientNetworks(updateSerial, filteredClientRoutes)
|
|
||||||
m.notifier.OnNewRoutes(filteredClientRoutes)
|
|
||||||
|
|
||||||
if m.serverRouter != nil {
|
|
||||||
err := m.serverRouter.updateRoutes(newServerRoutesMap)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("update routes: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newServerRoutesMap, newClientRoutesIDMap, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.mux.Lock()
|
||||||
|
defer m.mux.Unlock()
|
||||||
|
m.useNewDNSRoute = useNewDNSRoute
|
||||||
|
|
||||||
|
newServerRoutesMap, newClientRoutesIDMap := m.classifyRoutes(newRoutes)
|
||||||
|
|
||||||
|
filteredClientRoutes := m.routeSelector.FilterSelected(newClientRoutesIDMap)
|
||||||
|
m.updateClientNetworks(updateSerial, filteredClientRoutes)
|
||||||
|
m.notifier.OnNewRoutes(filteredClientRoutes)
|
||||||
|
|
||||||
|
if m.serverRouter != nil {
|
||||||
|
err := m.serverRouter.updateRoutes(newServerRoutesMap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.clientRoutes = newClientRoutesIDMap
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRouteChangeListener set RouteListener for route change Notifier
|
// SetRouteChangeListener set RouteListener for route change Notifier
|
||||||
@@ -225,9 +273,24 @@ func (m *DefaultManager) GetRouteSelector() *routeselector.RouteSelector {
|
|||||||
return m.routeSelector
|
return m.routeSelector
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClientRoutes returns the client routes
|
// GetClientRoutes returns most recent list of clientRoutes received from the Management Service
|
||||||
func (m *DefaultManager) GetClientRoutes() map[route.HAUniqueID]*clientNetwork {
|
func (m *DefaultManager) GetClientRoutes() route.HAMap {
|
||||||
return m.clientNetworks
|
m.mux.Lock()
|
||||||
|
defer m.mux.Unlock()
|
||||||
|
|
||||||
|
return maps.Clone(m.clientRoutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientRoutesWithNetID returns the current routes from the route map, but the keys consist of the network ID only
|
||||||
|
func (m *DefaultManager) GetClientRoutesWithNetID() map[route.NetID][]*route.Route {
|
||||||
|
m.mux.Lock()
|
||||||
|
defer m.mux.Unlock()
|
||||||
|
|
||||||
|
routes := make(map[route.NetID][]*route.Route, len(m.clientRoutes))
|
||||||
|
for id, v := range m.clientRoutes {
|
||||||
|
routes[id.NetID()] = v
|
||||||
|
}
|
||||||
|
return routes
|
||||||
}
|
}
|
||||||
|
|
||||||
// TriggerSelection triggers the selection of routes, stopping deselected watchers and starting newly selected ones
|
// TriggerSelection triggers the selection of routes, stopping deselected watchers and starting newly selected ones
|
||||||
@@ -247,11 +310,26 @@ func (m *DefaultManager) TriggerSelection(networks route.HAMap) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
clientNetworkWatcher := newClientNetworkWatcher(m.ctx, m.dnsRouteInterval, m.wgInterface, m.statusRecorder, routes[0], m.routeRefCounter, m.allowedIPsRefCounter)
|
clientNetworkWatcher := newClientNetworkWatcher(
|
||||||
|
m.ctx,
|
||||||
|
m.dnsRouteInterval,
|
||||||
|
m.wgInterface,
|
||||||
|
m.statusRecorder,
|
||||||
|
routes[0],
|
||||||
|
m.routeRefCounter,
|
||||||
|
m.allowedIPsRefCounter,
|
||||||
|
m.dnsServer,
|
||||||
|
m.peerStore,
|
||||||
|
m.useNewDNSRoute,
|
||||||
|
)
|
||||||
m.clientNetworks[id] = clientNetworkWatcher
|
m.clientNetworks[id] = clientNetworkWatcher
|
||||||
go clientNetworkWatcher.peersStateAndUpdateWatcher()
|
go clientNetworkWatcher.peersStateAndUpdateWatcher()
|
||||||
clientNetworkWatcher.sendUpdateToClientNetworkWatcher(routesUpdate{routes: routes})
|
clientNetworkWatcher.sendUpdateToClientNetworkWatcher(routesUpdate{routes: routes})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := m.stateManager.UpdateState((*SelectorState)(m.routeSelector)); err != nil {
|
||||||
|
log.Errorf("failed to update state: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// stopObsoleteClients stops the client network watcher for the networks that are not in the new list
|
// stopObsoleteClients stops the client network watcher for the networks that are not in the new list
|
||||||
@@ -272,7 +350,18 @@ func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks rout
|
|||||||
for id, routes := range networks {
|
for id, routes := range networks {
|
||||||
clientNetworkWatcher, found := m.clientNetworks[id]
|
clientNetworkWatcher, found := m.clientNetworks[id]
|
||||||
if !found {
|
if !found {
|
||||||
clientNetworkWatcher = newClientNetworkWatcher(m.ctx, m.dnsRouteInterval, m.wgInterface, m.statusRecorder, routes[0], m.routeRefCounter, m.allowedIPsRefCounter)
|
clientNetworkWatcher = newClientNetworkWatcher(
|
||||||
|
m.ctx,
|
||||||
|
m.dnsRouteInterval,
|
||||||
|
m.wgInterface,
|
||||||
|
m.statusRecorder,
|
||||||
|
routes[0],
|
||||||
|
m.routeRefCounter,
|
||||||
|
m.allowedIPsRefCounter,
|
||||||
|
m.dnsServer,
|
||||||
|
m.peerStore,
|
||||||
|
m.useNewDNSRoute,
|
||||||
|
)
|
||||||
m.clientNetworks[id] = clientNetworkWatcher
|
m.clientNetworks[id] = clientNetworkWatcher
|
||||||
go clientNetworkWatcher.peersStateAndUpdateWatcher()
|
go clientNetworkWatcher.peersStateAndUpdateWatcher()
|
||||||
}
|
}
|
||||||
@@ -315,7 +404,7 @@ func (m *DefaultManager) classifyRoutes(newRoutes []*route.Route) (map[route.ID]
|
|||||||
return newServerRoutesMap, newClientRoutesIDMap
|
return newServerRoutesMap, newClientRoutesIDMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DefaultManager) clientRoutes(initialRoutes []*route.Route) []*route.Route {
|
func (m *DefaultManager) initialClientRoutes(initialRoutes []*route.Route) []*route.Route {
|
||||||
_, crMap := m.classifyRoutes(initialRoutes)
|
_, crMap := m.classifyRoutes(initialRoutes)
|
||||||
rs := make([]*route.Route, 0, len(crMap))
|
rs := make([]*route.Route, 0, len(crMap))
|
||||||
for _, routes := range crMap {
|
for _, routes := range crMap {
|
||||||
|
|||||||
@@ -424,9 +424,9 @@ func TestManagerUpdateRoutes(t *testing.T) {
|
|||||||
|
|
||||||
statusRecorder := peer.NewRecorder("https://mgm")
|
statusRecorder := peer.NewRecorder("https://mgm")
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
routeManager := NewManager(ctx, localPeerKey, 0, wgInterface, statusRecorder, nil, nil)
|
routeManager := NewManager(ctx, localPeerKey, 0, wgInterface, statusRecorder, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
_, _, err = routeManager.Init(nil)
|
_, _, err = routeManager.Init()
|
||||||
|
|
||||||
require.NoError(t, err, "should init route manager")
|
require.NoError(t, err, "should init route manager")
|
||||||
defer routeManager.Stop(nil)
|
defer routeManager.Stop(nil)
|
||||||
@@ -436,11 +436,11 @@ func TestManagerUpdateRoutes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(testCase.inputInitRoutes) > 0 {
|
if len(testCase.inputInitRoutes) > 0 {
|
||||||
_, _, err = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes)
|
_ = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes, false)
|
||||||
require.NoError(t, err, "should update routes with init routes")
|
require.NoError(t, err, "should update routes with init routes")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), testCase.inputRoutes)
|
_ = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), testCase.inputRoutes, false)
|
||||||
require.NoError(t, err, "should update routes")
|
require.NoError(t, err, "should update routes")
|
||||||
|
|
||||||
expectedWatchers := testCase.clientNetworkWatchersExpected
|
expectedWatchers := testCase.clientNetworkWatchersExpected
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package routemanager
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
@@ -15,13 +14,15 @@ import (
|
|||||||
|
|
||||||
// MockManager is the mock instance of a route manager
|
// MockManager is the mock instance of a route manager
|
||||||
type MockManager struct {
|
type MockManager struct {
|
||||||
UpdateRoutesFunc func(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, error)
|
UpdateRoutesFunc func(updateSerial uint64, newRoutes []*route.Route) error
|
||||||
TriggerSelectionFunc func(haMap route.HAMap)
|
TriggerSelectionFunc func(haMap route.HAMap)
|
||||||
GetRouteSelectorFunc func() *routeselector.RouteSelector
|
GetRouteSelectorFunc func() *routeselector.RouteSelector
|
||||||
StopFunc func(manager *statemanager.Manager)
|
GetClientRoutesFunc func() route.HAMap
|
||||||
|
GetClientRoutesWithNetIDFunc func() map[route.NetID][]*route.Route
|
||||||
|
StopFunc func(manager *statemanager.Manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockManager) Init(*statemanager.Manager) (net.AddHookFunc, net.RemoveHookFunc, error) {
|
func (m *MockManager) Init() (net.AddHookFunc, net.RemoveHookFunc, error) {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,11 +32,11 @@ func (m *MockManager) InitialRouteRange() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRoutes mock implementation of UpdateRoutes from Manager interface
|
// UpdateRoutes mock implementation of UpdateRoutes from Manager interface
|
||||||
func (m *MockManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, error) {
|
func (m *MockManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route, b bool) error {
|
||||||
if m.UpdateRoutesFunc != nil {
|
if m.UpdateRoutesFunc != nil {
|
||||||
return m.UpdateRoutesFunc(updateSerial, newRoutes)
|
return m.UpdateRoutesFunc(updateSerial, newRoutes)
|
||||||
}
|
}
|
||||||
return nil, nil, fmt.Errorf("method UpdateRoutes is not implemented")
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockManager) TriggerSelection(networks route.HAMap) {
|
func (m *MockManager) TriggerSelection(networks route.HAMap) {
|
||||||
@@ -52,6 +53,22 @@ func (m *MockManager) GetRouteSelector() *routeselector.RouteSelector {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetClientRoutes mock implementation of GetClientRoutes from Manager interface
|
||||||
|
func (m *MockManager) GetClientRoutes() route.HAMap {
|
||||||
|
if m.GetClientRoutesFunc != nil {
|
||||||
|
return m.GetClientRoutesFunc()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientRoutesWithNetID mock implementation of GetClientRoutesWithNetID from Manager interface
|
||||||
|
func (m *MockManager) GetClientRoutesWithNetID() map[route.NetID][]*route.Route {
|
||||||
|
if m.GetClientRoutesWithNetIDFunc != nil {
|
||||||
|
return m.GetClientRoutesWithNetIDFunc()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Start mock implementation of Start from Manager interface
|
// Start mock implementation of Start from Manager interface
|
||||||
func (m *MockManager) Start(ctx context.Context, iface *iface.WGIface) {
|
func (m *MockManager) Start(ctx context.Context, iface *iface.WGIface) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,10 +47,9 @@ type RemoveFunc[Key, O any] func(key Key, out O) error
|
|||||||
type Counter[Key comparable, I, O any] struct {
|
type Counter[Key comparable, I, O any] struct {
|
||||||
// refCountMap keeps track of the reference Ref for keys
|
// refCountMap keeps track of the reference Ref for keys
|
||||||
refCountMap map[Key]Ref[O]
|
refCountMap map[Key]Ref[O]
|
||||||
refCountMu sync.Mutex
|
mu sync.Mutex
|
||||||
// idMap keeps track of the keys associated with an ID for removal
|
// idMap keeps track of the keys associated with an ID for removal
|
||||||
idMap map[string][]Key
|
idMap map[string][]Key
|
||||||
idMu sync.Mutex
|
|
||||||
add AddFunc[Key, I, O]
|
add AddFunc[Key, I, O]
|
||||||
remove RemoveFunc[Key, O]
|
remove RemoveFunc[Key, O]
|
||||||
}
|
}
|
||||||
@@ -72,13 +71,14 @@ func New[Key comparable, I, O any](add AddFunc[Key, I, O], remove RemoveFunc[Key
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadData loads the data from the existing counter
|
// LoadData loads the data from the existing counter
|
||||||
|
// The passed counter should not be used any longer after calling this function.
|
||||||
func (rm *Counter[Key, I, O]) LoadData(
|
func (rm *Counter[Key, I, O]) LoadData(
|
||||||
existingCounter *Counter[Key, I, O],
|
existingCounter *Counter[Key, I, O],
|
||||||
) {
|
) {
|
||||||
rm.refCountMu.Lock()
|
rm.mu.Lock()
|
||||||
defer rm.refCountMu.Unlock()
|
defer rm.mu.Unlock()
|
||||||
rm.idMu.Lock()
|
existingCounter.mu.Lock()
|
||||||
defer rm.idMu.Unlock()
|
defer existingCounter.mu.Unlock()
|
||||||
|
|
||||||
rm.refCountMap = existingCounter.refCountMap
|
rm.refCountMap = existingCounter.refCountMap
|
||||||
rm.idMap = existingCounter.idMap
|
rm.idMap = existingCounter.idMap
|
||||||
@@ -87,8 +87,8 @@ func (rm *Counter[Key, I, O]) LoadData(
|
|||||||
// Get retrieves the current reference count and associated data for a key.
|
// Get retrieves the current reference count and associated data for a key.
|
||||||
// If the key doesn't exist, it returns a zero value Ref and false.
|
// If the key doesn't exist, it returns a zero value Ref and false.
|
||||||
func (rm *Counter[Key, I, O]) Get(key Key) (Ref[O], bool) {
|
func (rm *Counter[Key, I, O]) Get(key Key) (Ref[O], bool) {
|
||||||
rm.refCountMu.Lock()
|
rm.mu.Lock()
|
||||||
defer rm.refCountMu.Unlock()
|
defer rm.mu.Unlock()
|
||||||
|
|
||||||
ref, ok := rm.refCountMap[key]
|
ref, ok := rm.refCountMap[key]
|
||||||
return ref, ok
|
return ref, ok
|
||||||
@@ -97,9 +97,13 @@ func (rm *Counter[Key, I, O]) Get(key Key) (Ref[O], bool) {
|
|||||||
// Increment increments the reference count for the given key.
|
// Increment increments the reference count for the given key.
|
||||||
// If this is the first reference to the key, the AddFunc is called.
|
// If this is the first reference to the key, the AddFunc is called.
|
||||||
func (rm *Counter[Key, I, O]) Increment(key Key, in I) (Ref[O], error) {
|
func (rm *Counter[Key, I, O]) Increment(key Key, in I) (Ref[O], error) {
|
||||||
rm.refCountMu.Lock()
|
rm.mu.Lock()
|
||||||
defer rm.refCountMu.Unlock()
|
defer rm.mu.Unlock()
|
||||||
|
|
||||||
|
return rm.increment(key, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *Counter[Key, I, O]) increment(key Key, in I) (Ref[O], error) {
|
||||||
ref := rm.refCountMap[key]
|
ref := rm.refCountMap[key]
|
||||||
logCallerF("Increasing ref count [%d -> %d] for key %v with In [%v] Out [%v]", ref.Count, ref.Count+1, key, in, ref.Out)
|
logCallerF("Increasing ref count [%d -> %d] for key %v with In [%v] Out [%v]", ref.Count, ref.Count+1, key, in, ref.Out)
|
||||||
|
|
||||||
@@ -126,10 +130,10 @@ func (rm *Counter[Key, I, O]) Increment(key Key, in I) (Ref[O], error) {
|
|||||||
// IncrementWithID increments the reference count for the given key and groups it under the given ID.
|
// IncrementWithID increments the reference count for the given key and groups it under the given ID.
|
||||||
// If this is the first reference to the key, the AddFunc is called.
|
// If this is the first reference to the key, the AddFunc is called.
|
||||||
func (rm *Counter[Key, I, O]) IncrementWithID(id string, key Key, in I) (Ref[O], error) {
|
func (rm *Counter[Key, I, O]) IncrementWithID(id string, key Key, in I) (Ref[O], error) {
|
||||||
rm.idMu.Lock()
|
rm.mu.Lock()
|
||||||
defer rm.idMu.Unlock()
|
defer rm.mu.Unlock()
|
||||||
|
|
||||||
ref, err := rm.Increment(key, in)
|
ref, err := rm.increment(key, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ref, fmt.Errorf("with ID: %w", err)
|
return ref, fmt.Errorf("with ID: %w", err)
|
||||||
}
|
}
|
||||||
@@ -141,9 +145,12 @@ func (rm *Counter[Key, I, O]) IncrementWithID(id string, key Key, in I) (Ref[O],
|
|||||||
// Decrement decrements the reference count for the given key.
|
// Decrement decrements the reference count for the given key.
|
||||||
// If the reference count reaches 0, the RemoveFunc is called.
|
// If the reference count reaches 0, the RemoveFunc is called.
|
||||||
func (rm *Counter[Key, I, O]) Decrement(key Key) (Ref[O], error) {
|
func (rm *Counter[Key, I, O]) Decrement(key Key) (Ref[O], error) {
|
||||||
rm.refCountMu.Lock()
|
rm.mu.Lock()
|
||||||
defer rm.refCountMu.Unlock()
|
defer rm.mu.Unlock()
|
||||||
|
return rm.decrement(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *Counter[Key, I, O]) decrement(key Key) (Ref[O], error) {
|
||||||
ref, ok := rm.refCountMap[key]
|
ref, ok := rm.refCountMap[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
logCallerF("No reference found for key %v", key)
|
logCallerF("No reference found for key %v", key)
|
||||||
@@ -168,12 +175,12 @@ func (rm *Counter[Key, I, O]) Decrement(key Key) (Ref[O], error) {
|
|||||||
// DecrementWithID decrements the reference count for all keys associated with the given ID.
|
// DecrementWithID decrements the reference count for all keys associated with the given ID.
|
||||||
// If the reference count reaches 0, the RemoveFunc is called.
|
// If the reference count reaches 0, the RemoveFunc is called.
|
||||||
func (rm *Counter[Key, I, O]) DecrementWithID(id string) error {
|
func (rm *Counter[Key, I, O]) DecrementWithID(id string) error {
|
||||||
rm.idMu.Lock()
|
rm.mu.Lock()
|
||||||
defer rm.idMu.Unlock()
|
defer rm.mu.Unlock()
|
||||||
|
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
for _, key := range rm.idMap[id] {
|
for _, key := range rm.idMap[id] {
|
||||||
if _, err := rm.Decrement(key); err != nil {
|
if _, err := rm.decrement(key); err != nil {
|
||||||
merr = multierror.Append(merr, err)
|
merr = multierror.Append(merr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,10 +191,8 @@ func (rm *Counter[Key, I, O]) DecrementWithID(id string) error {
|
|||||||
|
|
||||||
// Flush removes all references and calls RemoveFunc for each key.
|
// Flush removes all references and calls RemoveFunc for each key.
|
||||||
func (rm *Counter[Key, I, O]) Flush() error {
|
func (rm *Counter[Key, I, O]) Flush() error {
|
||||||
rm.refCountMu.Lock()
|
rm.mu.Lock()
|
||||||
defer rm.refCountMu.Unlock()
|
defer rm.mu.Unlock()
|
||||||
rm.idMu.Lock()
|
|
||||||
defer rm.idMu.Unlock()
|
|
||||||
|
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
for key := range rm.refCountMap {
|
for key := range rm.refCountMap {
|
||||||
@@ -206,10 +211,8 @@ func (rm *Counter[Key, I, O]) Flush() error {
|
|||||||
|
|
||||||
// Clear removes all references without calling RemoveFunc.
|
// Clear removes all references without calling RemoveFunc.
|
||||||
func (rm *Counter[Key, I, O]) Clear() {
|
func (rm *Counter[Key, I, O]) Clear() {
|
||||||
rm.refCountMu.Lock()
|
rm.mu.Lock()
|
||||||
defer rm.refCountMu.Unlock()
|
defer rm.mu.Unlock()
|
||||||
rm.idMu.Lock()
|
|
||||||
defer rm.idMu.Unlock()
|
|
||||||
|
|
||||||
clear(rm.refCountMap)
|
clear(rm.refCountMap)
|
||||||
clear(rm.idMap)
|
clear(rm.idMap)
|
||||||
@@ -217,6 +220,9 @@ func (rm *Counter[Key, I, O]) Clear() {
|
|||||||
|
|
||||||
// MarshalJSON implements the json.Marshaler interface for Counter.
|
// MarshalJSON implements the json.Marshaler interface for Counter.
|
||||||
func (rm *Counter[Key, I, O]) MarshalJSON() ([]byte, error) {
|
func (rm *Counter[Key, I, O]) MarshalJSON() ([]byte, error) {
|
||||||
|
rm.mu.Lock()
|
||||||
|
defer rm.mu.Unlock()
|
||||||
|
|
||||||
return json.Marshal(struct {
|
return json.Marshal(struct {
|
||||||
RefCountMap map[Key]Ref[O] `json:"refCountMap"`
|
RefCountMap map[Key]Ref[O] `json:"refCountMap"`
|
||||||
IDMap map[string][]Key `json:"idMap"`
|
IDMap map[string][]Key `json:"idMap"`
|
||||||
@@ -228,6 +234,9 @@ func (rm *Counter[Key, I, O]) MarshalJSON() ([]byte, error) {
|
|||||||
|
|
||||||
// UnmarshalJSON implements the json.Unmarshaler interface for Counter.
|
// UnmarshalJSON implements the json.Unmarshaler interface for Counter.
|
||||||
func (rm *Counter[Key, I, O]) UnmarshalJSON(data []byte) error {
|
func (rm *Counter[Key, I, O]) UnmarshalJSON(data []byte) error {
|
||||||
|
rm.mu.Lock()
|
||||||
|
defer rm.mu.Unlock()
|
||||||
|
|
||||||
var temp struct {
|
var temp struct {
|
||||||
RefCountMap map[Key]Ref[O] `json:"refCountMap"`
|
RefCountMap map[Key]Ref[O] `json:"refCountMap"`
|
||||||
IDMap map[string][]Key `json:"idMap"`
|
IDMap map[string][]Key `json:"idMap"`
|
||||||
@@ -238,6 +247,13 @@ func (rm *Counter[Key, I, O]) UnmarshalJSON(data []byte) error {
|
|||||||
rm.refCountMap = temp.RefCountMap
|
rm.refCountMap = temp.RefCountMap
|
||||||
rm.idMap = temp.IDMap
|
rm.idMap = temp.IDMap
|
||||||
|
|
||||||
|
if temp.RefCountMap == nil {
|
||||||
|
temp.RefCountMap = map[Key]Ref[O]{}
|
||||||
|
}
|
||||||
|
if temp.IDMap == nil {
|
||||||
|
temp.IDMap = map[string][]Key{}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
client/internal/routemanager/state.go
Normal file
19
client/internal/routemanager/state.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routeselector"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SelectorState routeselector.RouteSelector
|
||||||
|
|
||||||
|
func (s *SelectorState) Name() string {
|
||||||
|
return "routeselector_state"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SelectorState) MarshalJSON() ([]byte, error) {
|
||||||
|
return (*routeselector.RouteSelector)(s).MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SelectorState) UnmarshalJSON(data []byte) error {
|
||||||
|
return (*routeselector.RouteSelector)(s).UnmarshalJSON(data)
|
||||||
|
}
|
||||||
@@ -2,31 +2,28 @@ package systemops
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShutdownState struct {
|
type ShutdownState ExclusionCounter
|
||||||
Counter *ExclusionCounter `json:"counter,omitempty"`
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ShutdownState) Name() string {
|
func (s *ShutdownState) Name() string {
|
||||||
return "route_state"
|
return "route_state"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShutdownState) Cleanup() error {
|
func (s *ShutdownState) Cleanup() error {
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
if s.Counter == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sysops := NewSysOps(nil, nil)
|
sysops := NewSysOps(nil, nil)
|
||||||
sysops.refCounter = refcounter.New[netip.Prefix, struct{}, Nexthop](nil, sysops.removeFromRouteTable)
|
sysops.refCounter = refcounter.New[netip.Prefix, struct{}, Nexthop](nil, sysops.removeFromRouteTable)
|
||||||
sysops.refCounter.LoadData(s.Counter)
|
sysops.refCounter.LoadData((*ExclusionCounter)(s))
|
||||||
|
|
||||||
return sysops.refCounter.Flush()
|
return sysops.refCounter.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ShutdownState) MarshalJSON() ([]byte, error) {
|
||||||
|
return (*ExclusionCounter)(s).MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShutdownState) UnmarshalJSON(data []byte) error {
|
||||||
|
return (*ExclusionCounter)(s).UnmarshalJSON(data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,30 +57,19 @@ func (r *SysOps) setupRefCounter(initAddresses []net.IP, stateManager *statemana
|
|||||||
return nexthop, refcounter.ErrIgnore
|
return nexthop, refcounter.ErrIgnore
|
||||||
}
|
}
|
||||||
|
|
||||||
r.updateState(stateManager)
|
|
||||||
|
|
||||||
return nexthop, err
|
return nexthop, err
|
||||||
},
|
},
|
||||||
func(prefix netip.Prefix, nexthop Nexthop) error {
|
r.removeFromRouteTable,
|
||||||
// remove from state even if we have trouble removing it from the route table
|
|
||||||
// it could be already gone
|
|
||||||
r.updateState(stateManager)
|
|
||||||
|
|
||||||
return r.removeFromRouteTable(prefix, nexthop)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
r.refCounter = refCounter
|
r.refCounter = refCounter
|
||||||
|
|
||||||
return r.setupHooks(initAddresses)
|
return r.setupHooks(initAddresses, stateManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateState updates state on every change so it will be persisted regularly
|
||||||
func (r *SysOps) updateState(stateManager *statemanager.Manager) {
|
func (r *SysOps) updateState(stateManager *statemanager.Manager) {
|
||||||
state := getState(stateManager)
|
if err := stateManager.UpdateState((*ShutdownState)(r.refCounter)); err != nil {
|
||||||
|
|
||||||
state.Counter = r.refCounter
|
|
||||||
|
|
||||||
if err := stateManager.UpdateState(state); err != nil {
|
|
||||||
log.Errorf("failed to update state: %v", err)
|
log.Errorf("failed to update state: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,7 +325,7 @@ func (r *SysOps) genericRemoveVPNRoute(prefix netip.Prefix, intf *net.Interface)
|
|||||||
return r.removeFromRouteTable(prefix, nextHop)
|
return r.removeFromRouteTable(prefix, nextHop)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SysOps) setupHooks(initAddresses []net.IP) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) {
|
func (r *SysOps) setupHooks(initAddresses []net.IP, stateManager *statemanager.Manager) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) {
|
||||||
beforeHook := func(connID nbnet.ConnectionID, ip net.IP) error {
|
beforeHook := func(connID nbnet.ConnectionID, ip net.IP) error {
|
||||||
prefix, err := util.GetPrefixFromIP(ip)
|
prefix, err := util.GetPrefixFromIP(ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -347,6 +336,8 @@ func (r *SysOps) setupHooks(initAddresses []net.IP) (nbnet.AddHookFunc, nbnet.Re
|
|||||||
return fmt.Errorf("adding route reference: %v", err)
|
return fmt.Errorf("adding route reference: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.updateState(stateManager)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
afterHook := func(connID nbnet.ConnectionID) error {
|
afterHook := func(connID nbnet.ConnectionID) error {
|
||||||
@@ -354,6 +345,8 @@ func (r *SysOps) setupHooks(initAddresses []net.IP) (nbnet.AddHookFunc, nbnet.Re
|
|||||||
return fmt.Errorf("remove route reference: %w", err)
|
return fmt.Errorf("remove route reference: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.updateState(stateManager)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,14 +525,3 @@ func isVpnRoute(addr netip.Addr, vpnRoutes []netip.Prefix, localRoutes []netip.P
|
|||||||
// Return true if the longest matching prefix is from vpnRoutes
|
// Return true if the longest matching prefix is from vpnRoutes
|
||||||
return isVpn, longestPrefix
|
return isVpn, longestPrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
func getState(stateManager *statemanager.Manager) *ShutdownState {
|
|
||||||
var shutdownState *ShutdownState
|
|
||||||
if state := stateManager.GetState(shutdownState); state != nil {
|
|
||||||
shutdownState = state.(*ShutdownState)
|
|
||||||
} else {
|
|
||||||
shutdownState = &ShutdownState{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return shutdownState
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ type ruleParams struct {
|
|||||||
|
|
||||||
// isLegacy determines whether to use the legacy routing setup
|
// isLegacy determines whether to use the legacy routing setup
|
||||||
func isLegacy() bool {
|
func isLegacy() bool {
|
||||||
return os.Getenv("NB_USE_LEGACY_ROUTING") == "true" || nbnet.CustomRoutingDisabled()
|
return os.Getenv("NB_USE_LEGACY_ROUTING") == "true" || nbnet.CustomRoutingDisabled() || nbnet.SkipSocketMark()
|
||||||
}
|
}
|
||||||
|
|
||||||
// setIsLegacy sets the legacy routing setup
|
// setIsLegacy sets the legacy routing setup
|
||||||
@@ -92,17 +92,6 @@ func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager
|
|||||||
return r.setupRefCounter(initAddresses, stateManager)
|
return r.setupRefCounter(initAddresses, stateManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = addRoutingTableName(); err != nil {
|
|
||||||
log.Errorf("Error adding routing table name: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
originalValues, err := sysctl.Setup(r.wgInterface)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error setting up sysctl: %v", err)
|
|
||||||
sysctlFailed = true
|
|
||||||
}
|
|
||||||
originalSysctl = originalValues
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if cleanErr := r.CleanupRouting(stateManager); cleanErr != nil {
|
if cleanErr := r.CleanupRouting(stateManager); cleanErr != nil {
|
||||||
@@ -123,6 +112,17 @@ func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = addRoutingTableName(); err != nil {
|
||||||
|
log.Errorf("Error adding routing table name: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalValues, err := sysctl.Setup(r.wgInterface)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error setting up sysctl: %v", err)
|
||||||
|
sysctlFailed = true
|
||||||
|
}
|
||||||
|
originalSysctl = originalValues
|
||||||
|
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,10 +230,13 @@ func (rm *RouteMonitor) parseUpdate(row *MIB_IPFORWARD_ROW2, notificationType MI
|
|||||||
if idx != 0 {
|
if idx != 0 {
|
||||||
intf, err := net.InterfaceByIndex(idx)
|
intf, err := net.InterfaceByIndex(idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return update, fmt.Errorf("get interface name: %w", err)
|
log.Warnf("failed to get interface name for index %d: %v", idx, err)
|
||||||
|
update.Interface = &net.Interface{
|
||||||
|
Index: idx,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
update.Interface = intf
|
||||||
}
|
}
|
||||||
|
|
||||||
update.Interface = intf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Tracef("Received route update with destination %v, next hop %v, interface %v", row.DestinationPrefix, row.NextHop, update.Interface)
|
log.Tracef("Received route update with destination %v, next hop %v, interface %v", row.DestinationPrefix, row.NextHop, update.Interface)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package routeselector
|
package routeselector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
@@ -12,6 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RouteSelector struct {
|
type RouteSelector struct {
|
||||||
|
mu sync.RWMutex
|
||||||
selectedRoutes map[route.NetID]struct{}
|
selectedRoutes map[route.NetID]struct{}
|
||||||
selectAll bool
|
selectAll bool
|
||||||
}
|
}
|
||||||
@@ -26,6 +29,9 @@ func NewRouteSelector() *RouteSelector {
|
|||||||
|
|
||||||
// SelectRoutes updates the selected routes based on the provided route IDs.
|
// SelectRoutes updates the selected routes based on the provided route IDs.
|
||||||
func (rs *RouteSelector) SelectRoutes(routes []route.NetID, appendRoute bool, allRoutes []route.NetID) error {
|
func (rs *RouteSelector) SelectRoutes(routes []route.NetID, appendRoute bool, allRoutes []route.NetID) error {
|
||||||
|
rs.mu.Lock()
|
||||||
|
defer rs.mu.Unlock()
|
||||||
|
|
||||||
if !appendRoute {
|
if !appendRoute {
|
||||||
rs.selectedRoutes = map[route.NetID]struct{}{}
|
rs.selectedRoutes = map[route.NetID]struct{}{}
|
||||||
}
|
}
|
||||||
@@ -46,6 +52,9 @@ func (rs *RouteSelector) SelectRoutes(routes []route.NetID, appendRoute bool, al
|
|||||||
|
|
||||||
// SelectAllRoutes sets the selector to select all routes.
|
// SelectAllRoutes sets the selector to select all routes.
|
||||||
func (rs *RouteSelector) SelectAllRoutes() {
|
func (rs *RouteSelector) SelectAllRoutes() {
|
||||||
|
rs.mu.Lock()
|
||||||
|
defer rs.mu.Unlock()
|
||||||
|
|
||||||
rs.selectAll = true
|
rs.selectAll = true
|
||||||
rs.selectedRoutes = map[route.NetID]struct{}{}
|
rs.selectedRoutes = map[route.NetID]struct{}{}
|
||||||
}
|
}
|
||||||
@@ -53,6 +62,9 @@ func (rs *RouteSelector) SelectAllRoutes() {
|
|||||||
// DeselectRoutes removes specific routes from the selection.
|
// DeselectRoutes removes specific routes from the selection.
|
||||||
// If the selector is in "select all" mode, it will transition to "select specific" mode.
|
// If the selector is in "select all" mode, it will transition to "select specific" mode.
|
||||||
func (rs *RouteSelector) DeselectRoutes(routes []route.NetID, allRoutes []route.NetID) error {
|
func (rs *RouteSelector) DeselectRoutes(routes []route.NetID, allRoutes []route.NetID) error {
|
||||||
|
rs.mu.Lock()
|
||||||
|
defer rs.mu.Unlock()
|
||||||
|
|
||||||
if rs.selectAll {
|
if rs.selectAll {
|
||||||
rs.selectAll = false
|
rs.selectAll = false
|
||||||
rs.selectedRoutes = map[route.NetID]struct{}{}
|
rs.selectedRoutes = map[route.NetID]struct{}{}
|
||||||
@@ -76,12 +88,18 @@ func (rs *RouteSelector) DeselectRoutes(routes []route.NetID, allRoutes []route.
|
|||||||
|
|
||||||
// DeselectAllRoutes deselects all routes, effectively disabling route selection.
|
// DeselectAllRoutes deselects all routes, effectively disabling route selection.
|
||||||
func (rs *RouteSelector) DeselectAllRoutes() {
|
func (rs *RouteSelector) DeselectAllRoutes() {
|
||||||
|
rs.mu.Lock()
|
||||||
|
defer rs.mu.Unlock()
|
||||||
|
|
||||||
rs.selectAll = false
|
rs.selectAll = false
|
||||||
rs.selectedRoutes = map[route.NetID]struct{}{}
|
rs.selectedRoutes = map[route.NetID]struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSelected checks if a specific route is selected.
|
// IsSelected checks if a specific route is selected.
|
||||||
func (rs *RouteSelector) IsSelected(routeID route.NetID) bool {
|
func (rs *RouteSelector) IsSelected(routeID route.NetID) bool {
|
||||||
|
rs.mu.RLock()
|
||||||
|
defer rs.mu.RUnlock()
|
||||||
|
|
||||||
if rs.selectAll {
|
if rs.selectAll {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -91,6 +109,9 @@ func (rs *RouteSelector) IsSelected(routeID route.NetID) bool {
|
|||||||
|
|
||||||
// FilterSelected removes unselected routes from the provided map.
|
// FilterSelected removes unselected routes from the provided map.
|
||||||
func (rs *RouteSelector) FilterSelected(routes route.HAMap) route.HAMap {
|
func (rs *RouteSelector) FilterSelected(routes route.HAMap) route.HAMap {
|
||||||
|
rs.mu.RLock()
|
||||||
|
defer rs.mu.RUnlock()
|
||||||
|
|
||||||
if rs.selectAll {
|
if rs.selectAll {
|
||||||
return maps.Clone(routes)
|
return maps.Clone(routes)
|
||||||
}
|
}
|
||||||
@@ -103,3 +124,49 @@ func (rs *RouteSelector) FilterSelected(routes route.HAMap) route.HAMap {
|
|||||||
}
|
}
|
||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface
|
||||||
|
func (rs *RouteSelector) MarshalJSON() ([]byte, error) {
|
||||||
|
rs.mu.RLock()
|
||||||
|
defer rs.mu.RUnlock()
|
||||||
|
|
||||||
|
return json.Marshal(struct {
|
||||||
|
SelectedRoutes map[route.NetID]struct{} `json:"selected_routes"`
|
||||||
|
SelectAll bool `json:"select_all"`
|
||||||
|
}{
|
||||||
|
SelectAll: rs.selectAll,
|
||||||
|
SelectedRoutes: rs.selectedRoutes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Unmarshaler interface
|
||||||
|
// If the JSON is empty or null, it will initialize like a NewRouteSelector.
|
||||||
|
func (rs *RouteSelector) UnmarshalJSON(data []byte) error {
|
||||||
|
rs.mu.Lock()
|
||||||
|
defer rs.mu.Unlock()
|
||||||
|
|
||||||
|
// Check for null or empty JSON
|
||||||
|
if len(data) == 0 || string(data) == "null" {
|
||||||
|
rs.selectedRoutes = map[route.NetID]struct{}{}
|
||||||
|
rs.selectAll = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var temp struct {
|
||||||
|
SelectedRoutes map[route.NetID]struct{} `json:"selected_routes"`
|
||||||
|
SelectAll bool `json:"select_all"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &temp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.selectedRoutes = temp.SelectedRoutes
|
||||||
|
rs.selectAll = temp.SelectAll
|
||||||
|
|
||||||
|
if rs.selectedRoutes == nil {
|
||||||
|
rs.selectedRoutes = map[route.NetID]struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -273,3 +273,88 @@ func TestRouteSelector_FilterSelected(t *testing.T) {
|
|||||||
"route2|192.168.0.0/16": {},
|
"route2|192.168.0.0/16": {},
|
||||||
}, filtered)
|
}, filtered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRouteSelector_NewRoutesBehavior(t *testing.T) {
|
||||||
|
initialRoutes := []route.NetID{"route1", "route2", "route3"}
|
||||||
|
newRoutes := []route.NetID{"route1", "route2", "route3", "route4", "route5"}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initialState func(rs *routeselector.RouteSelector) error // Setup initial state
|
||||||
|
wantNewSelected []route.NetID // Expected selected routes after new routes appear
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "New routes with initial selectAll state",
|
||||||
|
initialState: func(rs *routeselector.RouteSelector) error {
|
||||||
|
rs.SelectAllRoutes()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
// When selectAll is true, all routes including new ones should be selected
|
||||||
|
wantNewSelected: []route.NetID{"route1", "route2", "route3", "route4", "route5"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "New routes after specific selection",
|
||||||
|
initialState: func(rs *routeselector.RouteSelector) error {
|
||||||
|
return rs.SelectRoutes([]route.NetID{"route1", "route2"}, false, initialRoutes)
|
||||||
|
},
|
||||||
|
// When specific routes were selected, new routes should remain unselected
|
||||||
|
wantNewSelected: []route.NetID{"route1", "route2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "New routes after deselect all",
|
||||||
|
initialState: func(rs *routeselector.RouteSelector) error {
|
||||||
|
rs.DeselectAllRoutes()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
// After deselect all, new routes should remain unselected
|
||||||
|
wantNewSelected: []route.NetID{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "New routes after deselecting specific routes",
|
||||||
|
initialState: func(rs *routeselector.RouteSelector) error {
|
||||||
|
rs.SelectAllRoutes()
|
||||||
|
return rs.DeselectRoutes([]route.NetID{"route1"}, initialRoutes)
|
||||||
|
},
|
||||||
|
// After deselecting specific routes, new routes should remain unselected
|
||||||
|
wantNewSelected: []route.NetID{"route2", "route3"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "New routes after selecting with append",
|
||||||
|
initialState: func(rs *routeselector.RouteSelector) error {
|
||||||
|
return rs.SelectRoutes([]route.NetID{"route1"}, true, initialRoutes)
|
||||||
|
},
|
||||||
|
// When routes were appended, new routes should remain unselected
|
||||||
|
wantNewSelected: []route.NetID{"route1"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
rs := routeselector.NewRouteSelector()
|
||||||
|
|
||||||
|
// Setup initial state
|
||||||
|
err := tt.initialState(rs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify selection state with new routes
|
||||||
|
for _, id := range newRoutes {
|
||||||
|
assert.Equal(t, rs.IsSelected(id), slices.Contains(tt.wantNewSelected, id),
|
||||||
|
"Route %s selection state incorrect", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional verification using FilterSelected
|
||||||
|
routes := route.HAMap{
|
||||||
|
"route1|10.0.0.0/8": {},
|
||||||
|
"route2|192.168.0.0/16": {},
|
||||||
|
"route3|172.16.0.0/12": {},
|
||||||
|
"route4|10.10.0.0/16": {},
|
||||||
|
"route5|192.168.1.0/24": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := rs.FilterSelected(routes)
|
||||||
|
expectedLen := len(tt.wantNewSelected)
|
||||||
|
assert.Equal(t, expectedLen, len(filtered),
|
||||||
|
"FilterSelected returned wrong number of routes, got %d want %d", len(filtered), expectedLen)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,14 +16,39 @@ import (
|
|||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errStateNotRegistered = "state %s not registered"
|
||||||
|
errLoadStateFile = "load state file: %w"
|
||||||
)
|
)
|
||||||
|
|
||||||
// State interface defines the methods that all state types must implement
|
// State interface defines the methods that all state types must implement
|
||||||
type State interface {
|
type State interface {
|
||||||
Name() string
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanableState interface extends State with cleanup capability
|
||||||
|
type CleanableState interface {
|
||||||
|
State
|
||||||
Cleanup() error
|
Cleanup() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RawState wraps raw JSON data for unregistered states
|
||||||
|
type RawState struct {
|
||||||
|
data json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RawState) Name() string {
|
||||||
|
return "" // This is a placeholder implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements json.Marshaler to preserve the original JSON
|
||||||
|
func (r *RawState) MarshalJSON() ([]byte, error) {
|
||||||
|
return r.data, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Manager handles the persistence and management of various states
|
// Manager handles the persistence and management of various states
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -73,15 +98,15 @@ func (m *Manager) Stop(ctx context.Context) error {
|
|||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
if m.cancel != nil {
|
if m.cancel == nil {
|
||||||
m.cancel()
|
return nil
|
||||||
|
}
|
||||||
|
m.cancel()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case <-m.done:
|
case <-m.done:
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -139,7 +164,7 @@ func (m *Manager) setState(name string, state State) error {
|
|||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
if _, exists := m.states[name]; !exists {
|
if _, exists := m.states[name]; !exists {
|
||||||
return fmt.Errorf("state %s not registered", name)
|
return fmt.Errorf(errStateNotRegistered, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.states[name] = state
|
m.states[name] = state
|
||||||
@@ -148,6 +173,63 @@ func (m *Manager) setState(name string, state State) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteStateByName handles deletion of states without cleanup.
|
||||||
|
// It doesn't require the state to be registered.
|
||||||
|
func (m *Manager) DeleteStateByName(stateName string) error {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
rawStates, err := m.loadStateFile(false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errLoadStateFile, err)
|
||||||
|
}
|
||||||
|
if rawStates == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := rawStates[stateName]; !exists {
|
||||||
|
return fmt.Errorf("state %s not found", stateName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark state as deleted by setting it to nil and marking it dirty
|
||||||
|
m.states[stateName] = nil
|
||||||
|
m.dirty[stateName] = struct{}{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAllStates removes all states.
|
||||||
|
func (m *Manager) DeleteAllStates() (int, error) {
|
||||||
|
if m == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
rawStates, err := m.loadStateFile(false)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf(errLoadStateFile, err)
|
||||||
|
}
|
||||||
|
if rawStates == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
count := len(rawStates)
|
||||||
|
|
||||||
|
// Mark all states as deleted and dirty
|
||||||
|
for name := range rawStates {
|
||||||
|
m.states[name] = nil
|
||||||
|
m.dirty[name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) periodicStateSave(ctx context.Context) {
|
func (m *Manager) periodicStateSave(ctx context.Context) {
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
@@ -178,25 +260,18 @@ func (m *Manager) PersistState(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
bs, err := marshalWithPanicRecovery(m.states)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal states: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
done := make(chan error, 1)
|
done := make(chan error, 1)
|
||||||
|
start := time.Now()
|
||||||
go func() {
|
go func() {
|
||||||
data, err := json.MarshalIndent(m.states, "", " ")
|
done <- util.WriteBytesWithRestrictedPermission(ctx, m.filePath, bs)
|
||||||
if err != nil {
|
|
||||||
done <- fmt.Errorf("marshal states: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// nolint:gosec
|
|
||||||
if err := os.WriteFile(m.filePath, data, 0640); err != nil {
|
|
||||||
done <- fmt.Errorf("write state file: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
done <- nil
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -208,63 +283,175 @@ func (m *Manager) PersistState(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("persisted shutdown states: %v", maps.Keys(m.dirty))
|
log.Debugf("persisted states: %v, took %v", maps.Keys(m.dirty), time.Since(start))
|
||||||
|
|
||||||
clear(m.dirty)
|
clear(m.dirty)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadState loads the existing state from the state file
|
// loadStateFile reads and unmarshals the state file into a map of raw JSON messages
|
||||||
func (m *Manager) loadState() error {
|
func (m *Manager) loadStateFile(deleteCorrupt bool) (map[string]json.RawMessage, error) {
|
||||||
data, err := os.ReadFile(m.filePath)
|
data, err := os.ReadFile(m.filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
log.Debug("state file does not exist")
|
log.Debug("state file does not exist")
|
||||||
return nil
|
return nil, nil // nolint:nilnil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("read state file: %w", err)
|
return nil, fmt.Errorf("read state file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var rawStates map[string]json.RawMessage
|
var rawStates map[string]json.RawMessage
|
||||||
if err := json.Unmarshal(data, &rawStates); err != nil {
|
if err := json.Unmarshal(data, &rawStates); err != nil {
|
||||||
log.Warn("State file appears to be corrupted, attempting to delete it")
|
if deleteCorrupt {
|
||||||
if err := os.Remove(m.filePath); err != nil {
|
log.Warn("State file appears to be corrupted, attempting to delete it", err)
|
||||||
log.Errorf("Failed to delete corrupted state file: %v", err)
|
if err := os.Remove(m.filePath); err != nil {
|
||||||
} else {
|
log.Errorf("Failed to delete corrupted state file: %v", err)
|
||||||
log.Info("State file deleted")
|
} else {
|
||||||
|
log.Info("State file deleted")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unmarshal states: %w", err)
|
return nil, fmt.Errorf("unmarshal states: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var merr *multierror.Error
|
return rawStates, nil
|
||||||
|
}
|
||||||
|
|
||||||
for name, rawState := range rawStates {
|
// loadSingleRawState unmarshals a raw state into a concrete state object
|
||||||
stateType, ok := m.stateTypes[name]
|
func (m *Manager) loadSingleRawState(name string, rawState json.RawMessage) (State, error) {
|
||||||
if !ok {
|
stateType, ok := m.stateTypes[name]
|
||||||
merr = multierror.Append(merr, fmt.Errorf("unknown state type: %s", name))
|
if !ok {
|
||||||
continue
|
return nil, fmt.Errorf(errStateNotRegistered, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if string(rawState) == "null" {
|
if string(rawState) == "null" {
|
||||||
continue
|
return nil, nil //nolint:nilnil
|
||||||
}
|
}
|
||||||
|
|
||||||
statePtr := reflect.New(stateType).Interface().(State)
|
statePtr := reflect.New(stateType).Interface().(State)
|
||||||
if err := json.Unmarshal(rawState, statePtr); err != nil {
|
if err := json.Unmarshal(rawState, statePtr); err != nil {
|
||||||
merr = multierror.Append(merr, fmt.Errorf("unmarshal state %s: %w", name, err))
|
return nil, fmt.Errorf("unmarshal state %s: %w", name, err)
|
||||||
continue
|
}
|
||||||
}
|
|
||||||
|
|
||||||
m.states[name] = statePtr
|
return statePtr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadState loads a specific state from the state file
|
||||||
|
func (m *Manager) LoadState(state State) error {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
rawStates, err := m.loadStateFile(false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rawStates == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name := state.Name()
|
||||||
|
rawState, exists := rawStates[name]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedState, err := m.loadSingleRawState(name, rawState)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.states[name] = loadedState
|
||||||
|
if loadedState != nil {
|
||||||
log.Debugf("loaded state: %s", name)
|
log.Debugf("loaded state: %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nberrors.FormatErrorOrNil(merr)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PerformCleanup retrieves all states from the state file for the registered states and calls Cleanup on them.
|
// cleanupSingleState handles the cleanup of a specific state and returns any error.
|
||||||
// If the cleanup is successful, the state is marked for deletion.
|
// The caller must hold the mutex.
|
||||||
|
func (m *Manager) cleanupSingleState(name string, rawState json.RawMessage) error {
|
||||||
|
// For unregistered states, preserve the raw JSON
|
||||||
|
if _, registered := m.stateTypes[name]; !registered {
|
||||||
|
m.states[name] = &RawState{data: rawState}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the state
|
||||||
|
loadedState, err := m.loadSingleRawState(name, rawState)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if loadedState == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if state supports cleanup
|
||||||
|
cleanableState, isCleanable := loadedState.(CleanableState)
|
||||||
|
if !isCleanable {
|
||||||
|
// If it doesn't support cleanup, keep it as-is
|
||||||
|
m.states[name] = loadedState
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform cleanup
|
||||||
|
log.Infof("cleaning up state %s", name)
|
||||||
|
if err := cleanableState.Cleanup(); err != nil {
|
||||||
|
// On cleanup error, preserve the state
|
||||||
|
m.states[name] = loadedState
|
||||||
|
return fmt.Errorf("cleanup state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Successfully cleaned up - mark for deletion
|
||||||
|
m.states[name] = nil
|
||||||
|
m.dirty[name] = struct{}{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupStateByName loads and cleans up a specific state by name if it implements CleanableState.
|
||||||
|
// Returns an error if the state doesn't exist, isn't registered, or cleanup fails.
|
||||||
|
func (m *Manager) CleanupStateByName(name string) error {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
// Check if state is registered
|
||||||
|
if _, registered := m.stateTypes[name]; !registered {
|
||||||
|
return fmt.Errorf(errStateNotRegistered, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load raw states from file
|
||||||
|
rawStates, err := m.loadStateFile(false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rawStates == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if state exists in file
|
||||||
|
rawState, exists := rawStates[name]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.cleanupSingleState(name, rawState); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PerformCleanup retrieves all states from the state file and calls Cleanup on registered states that support it.
|
||||||
|
// Unregistered states are preserved in their original state.
|
||||||
func (m *Manager) PerformCleanup() error {
|
func (m *Manager) PerformCleanup() error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -273,26 +460,63 @@ func (m *Manager) PerformCleanup() error {
|
|||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
if err := m.loadState(); err != nil {
|
// Load raw states from file
|
||||||
log.Warnf("Failed to load state during cleanup: %v", err)
|
rawStates, err := m.loadStateFile(true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errLoadStateFile, err)
|
||||||
|
}
|
||||||
|
if rawStates == nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
for name, state := range m.states {
|
|
||||||
if state == nil {
|
|
||||||
// If no state was found in the state file, we don't mark the state dirty nor return an error
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("client was not shut down properly, cleaning up %s", name)
|
// Process each state in the file
|
||||||
if err := state.Cleanup(); err != nil {
|
for name, rawState := range rawStates {
|
||||||
merr = multierror.Append(merr, fmt.Errorf("cleanup state for %s: %w", name, err))
|
if err := m.cleanupSingleState(name, rawState); err != nil {
|
||||||
} else {
|
merr = multierror.Append(merr, fmt.Errorf("%s: %w", name, err))
|
||||||
// mark for deletion on cleanup success
|
|
||||||
m.states[name] = nil
|
|
||||||
m.dirty[name] = struct{}{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSavedStateNames returns all state names that are currently saved in the state file.
|
||||||
|
func (m *Manager) GetSavedStateNames() ([]string, error) {
|
||||||
|
if m == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawStates, err := m.loadStateFile(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(errLoadStateFile, err)
|
||||||
|
}
|
||||||
|
if rawStates == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var states []string
|
||||||
|
for name, state := range rawStates {
|
||||||
|
if len(state) != 0 && string(state) != "null" {
|
||||||
|
states = append(states, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return states, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalWithPanicRecovery(v any) ([]byte, error) {
|
||||||
|
var bs []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("panic during marshal: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
bs, err = json.Marshal(v)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return bs, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,32 +4,20 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetDefaultStatePath returns the path to the state file based on the operating system
|
// GetDefaultStatePath returns the path to the state file based on the operating system
|
||||||
// It returns an empty string if the path cannot be determined. It also creates the directory if it does not exist.
|
// It returns an empty string if the path cannot be determined.
|
||||||
func GetDefaultStatePath() string {
|
func GetDefaultStatePath() string {
|
||||||
var path string
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "windows":
|
case "windows":
|
||||||
path = filepath.Join(os.Getenv("PROGRAMDATA"), "Netbird", "state.json")
|
return filepath.Join(os.Getenv("PROGRAMDATA"), "Netbird", "state.json")
|
||||||
case "darwin", "linux":
|
case "darwin", "linux":
|
||||||
path = "/var/lib/netbird/state.json"
|
return "/var/lib/netbird/state.json"
|
||||||
case "freebsd", "openbsd", "netbsd", "dragonfly":
|
case "freebsd", "openbsd", "netbsd", "dragonfly":
|
||||||
path = "/var/db/netbird/state.json"
|
return "/var/db/netbird/state.json"
|
||||||
// ios/android don't need state
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dir := filepath.Dir(path)
|
return ""
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
||||||
log.Errorf("Error creating directory %s: %v. Continuing without state support.", dir, err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ func init() {
|
|||||||
// Client struct manage the life circle of background service
|
// Client struct manage the life circle of background service
|
||||||
type Client struct {
|
type Client struct {
|
||||||
cfgFile string
|
cfgFile string
|
||||||
|
stateFile string
|
||||||
recorder *peer.Status
|
recorder *peer.Status
|
||||||
ctxCancel context.CancelFunc
|
ctxCancel context.CancelFunc
|
||||||
ctxCancelLock *sync.Mutex
|
ctxCancelLock *sync.Mutex
|
||||||
@@ -73,9 +74,10 @@ type Client struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewClient instantiate a new Client
|
// NewClient instantiate a new Client
|
||||||
func NewClient(cfgFile, deviceName string, osVersion string, osName string, networkChangeListener NetworkChangeListener, dnsManager DnsManager) *Client {
|
func NewClient(cfgFile, stateFile, deviceName string, osVersion string, osName string, networkChangeListener NetworkChangeListener, dnsManager DnsManager) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
cfgFile: cfgFile,
|
cfgFile: cfgFile,
|
||||||
|
stateFile: stateFile,
|
||||||
deviceName: deviceName,
|
deviceName: deviceName,
|
||||||
osName: osName,
|
osName: osName,
|
||||||
osVersion: osVersion,
|
osVersion: osVersion,
|
||||||
@@ -91,7 +93,8 @@ func (c *Client) Run(fd int32, interfaceName string) error {
|
|||||||
log.Infof("Starting NetBird client")
|
log.Infof("Starting NetBird client")
|
||||||
log.Debugf("Tunnel uses interface: %s", interfaceName)
|
log.Debugf("Tunnel uses interface: %s", interfaceName)
|
||||||
cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
|
cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
|
||||||
ConfigPath: c.cfgFile,
|
ConfigPath: c.cfgFile,
|
||||||
|
StateFilePath: c.stateFile,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -124,7 +127,7 @@ func (c *Client) Run(fd int32, interfaceName string) error {
|
|||||||
cfg.WgIface = interfaceName
|
cfg.WgIface = interfaceName
|
||||||
|
|
||||||
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
|
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
|
||||||
return c.connectClient.RunOniOS(fd, c.networkChangeListener, c.dnsManager)
|
return c.connectClient.RunOniOS(fd, c.networkChangeListener, c.dnsManager, c.stateFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the internal client and free the resources
|
// Stop the internal client and free the resources
|
||||||
@@ -269,8 +272,8 @@ func (c *Client) GetRoutesSelectionDetails() (*RoutesSelectionDetails, error) {
|
|||||||
return nil, fmt.Errorf("not connected")
|
return nil, fmt.Errorf("not connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
routesMap := engine.GetClientRoutesWithNetID()
|
|
||||||
routeManager := engine.GetRouteManager()
|
routeManager := engine.GetRouteManager()
|
||||||
|
routesMap := routeManager.GetClientRoutesWithNetID()
|
||||||
if routeManager == nil {
|
if routeManager == nil {
|
||||||
return nil, fmt.Errorf("could not get route manager")
|
return nil, fmt.Errorf("could not get route manager")
|
||||||
}
|
}
|
||||||
@@ -314,7 +317,7 @@ func (c *Client) GetRoutesSelectionDetails() (*RoutesSelectionDetails, error) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareRouteSelectionDetails(routes []*selectRoute, resolvedDomains map[domain.Domain][]netip.Prefix) *RoutesSelectionDetails {
|
func prepareRouteSelectionDetails(routes []*selectRoute, resolvedDomains map[domain.Domain]peer.ResolvedDomainInfo) *RoutesSelectionDetails {
|
||||||
var routeSelection []RoutesSelectionInfo
|
var routeSelection []RoutesSelectionInfo
|
||||||
for _, r := range routes {
|
for _, r := range routes {
|
||||||
domainList := make([]DomainInfo, 0)
|
domainList := make([]DomainInfo, 0)
|
||||||
@@ -322,9 +325,10 @@ func prepareRouteSelectionDetails(routes []*selectRoute, resolvedDomains map[dom
|
|||||||
domainResp := DomainInfo{
|
domainResp := DomainInfo{
|
||||||
Domain: d.SafeString(),
|
Domain: d.SafeString(),
|
||||||
}
|
}
|
||||||
if prefixes, exists := resolvedDomains[d]; exists {
|
|
||||||
|
if info, exists := resolvedDomains[d]; exists {
|
||||||
var ipStrings []string
|
var ipStrings []string
|
||||||
for _, prefix := range prefixes {
|
for _, prefix := range info.Prefixes {
|
||||||
ipStrings = append(ipStrings, prefix.Addr().String())
|
ipStrings = append(ipStrings, prefix.Addr().String())
|
||||||
}
|
}
|
||||||
domainResp.ResolvedIPs = strings.Join(ipStrings, ", ")
|
domainResp.ResolvedIPs = strings.Join(ipStrings, ", ")
|
||||||
@@ -362,12 +366,12 @@ func (c *Client) SelectRoute(id string) error {
|
|||||||
} else {
|
} else {
|
||||||
log.Debugf("select route with id: %s", id)
|
log.Debugf("select route with id: %s", id)
|
||||||
routes := toNetIDs([]string{id})
|
routes := toNetIDs([]string{id})
|
||||||
if err := routeSelector.SelectRoutes(routes, true, maps.Keys(engine.GetClientRoutesWithNetID())); err != nil {
|
if err := routeSelector.SelectRoutes(routes, true, maps.Keys(routeManager.GetClientRoutesWithNetID())); err != nil {
|
||||||
log.Debugf("error when selecting routes: %s", err)
|
log.Debugf("error when selecting routes: %s", err)
|
||||||
return fmt.Errorf("select routes: %w", err)
|
return fmt.Errorf("select routes: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
routeManager.TriggerSelection(engine.GetClientRoutes())
|
routeManager.TriggerSelection(routeManager.GetClientRoutes())
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -389,12 +393,12 @@ func (c *Client) DeselectRoute(id string) error {
|
|||||||
} else {
|
} else {
|
||||||
log.Debugf("deselect route with id: %s", id)
|
log.Debugf("deselect route with id: %s", id)
|
||||||
routes := toNetIDs([]string{id})
|
routes := toNetIDs([]string{id})
|
||||||
if err := routeSelector.DeselectRoutes(routes, maps.Keys(engine.GetClientRoutesWithNetID())); err != nil {
|
if err := routeSelector.DeselectRoutes(routes, maps.Keys(routeManager.GetClientRoutesWithNetID())); err != nil {
|
||||||
log.Debugf("error when deselecting routes: %s", err)
|
log.Debugf("error when deselecting routes: %s", err)
|
||||||
return fmt.Errorf("deselect routes: %w", err)
|
return fmt.Errorf("deselect routes: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
routeManager.TriggerSelection(engine.GetClientRoutes())
|
routeManager.TriggerSelection(routeManager.GetClientRoutes())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ type Preferences struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewPreferences create new Preferences instance
|
// NewPreferences create new Preferences instance
|
||||||
func NewPreferences(configPath string) *Preferences {
|
func NewPreferences(configPath string, stateFilePath string) *Preferences {
|
||||||
ci := internal.ConfigInput{
|
ci := internal.ConfigInput{
|
||||||
ConfigPath: configPath,
|
ConfigPath: configPath,
|
||||||
|
StateFilePath: stateFilePath,
|
||||||
}
|
}
|
||||||
return &Preferences{ci}
|
return &Preferences{ci}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import (
|
|||||||
|
|
||||||
func TestPreferences_DefaultValues(t *testing.T) {
|
func TestPreferences_DefaultValues(t *testing.T) {
|
||||||
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
|
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
|
||||||
p := NewPreferences(cfgFile)
|
stateFile := filepath.Join(t.TempDir(), "state.json")
|
||||||
|
p := NewPreferences(cfgFile, stateFile)
|
||||||
defaultVar, err := p.GetAdminURL()
|
defaultVar, err := p.GetAdminURL()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to read default value: %s", err)
|
t.Fatalf("failed to read default value: %s", err)
|
||||||
@@ -42,7 +43,8 @@ func TestPreferences_DefaultValues(t *testing.T) {
|
|||||||
func TestPreferences_ReadUncommitedValues(t *testing.T) {
|
func TestPreferences_ReadUncommitedValues(t *testing.T) {
|
||||||
exampleString := "exampleString"
|
exampleString := "exampleString"
|
||||||
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
|
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
|
||||||
p := NewPreferences(cfgFile)
|
stateFile := filepath.Join(t.TempDir(), "state.json")
|
||||||
|
p := NewPreferences(cfgFile, stateFile)
|
||||||
|
|
||||||
p.SetAdminURL(exampleString)
|
p.SetAdminURL(exampleString)
|
||||||
resp, err := p.GetAdminURL()
|
resp, err := p.GetAdminURL()
|
||||||
@@ -79,7 +81,8 @@ func TestPreferences_Commit(t *testing.T) {
|
|||||||
exampleURL := "https://myurl.com:443"
|
exampleURL := "https://myurl.com:443"
|
||||||
examplePresharedKey := "topsecret"
|
examplePresharedKey := "topsecret"
|
||||||
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
|
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
|
||||||
p := NewPreferences(cfgFile)
|
stateFile := filepath.Join(t.TempDir(), "state.json")
|
||||||
|
p := NewPreferences(cfgFile, stateFile)
|
||||||
|
|
||||||
p.SetAdminURL(exampleURL)
|
p.SetAdminURL(exampleURL)
|
||||||
p.SetManagementURL(exampleURL)
|
p.SetManagementURL(exampleURL)
|
||||||
@@ -90,7 +93,7 @@ func TestPreferences_Commit(t *testing.T) {
|
|||||||
t.Fatalf("failed to save changes: %s", err)
|
t.Fatalf("failed to save changes: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
p = NewPreferences(cfgFile)
|
p = NewPreferences(cfgFile, stateFile)
|
||||||
resp, err := p.GetAdminURL()
|
resp, err := p.GetAdminURL()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to read admin url: %s", err)
|
t.Fatalf("failed to read admin url: %s", err)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -28,14 +28,14 @@ service DaemonService {
|
|||||||
// GetConfig of the daemon.
|
// GetConfig of the daemon.
|
||||||
rpc GetConfig(GetConfigRequest) returns (GetConfigResponse) {}
|
rpc GetConfig(GetConfigRequest) returns (GetConfigResponse) {}
|
||||||
|
|
||||||
// List available network routes
|
// List available networks
|
||||||
rpc ListRoutes(ListRoutesRequest) returns (ListRoutesResponse) {}
|
rpc ListNetworks(ListNetworksRequest) returns (ListNetworksResponse) {}
|
||||||
|
|
||||||
// Select specific routes
|
// Select specific routes
|
||||||
rpc SelectRoutes(SelectRoutesRequest) returns (SelectRoutesResponse) {}
|
rpc SelectNetworks(SelectNetworksRequest) returns (SelectNetworksResponse) {}
|
||||||
|
|
||||||
// Deselect specific routes
|
// Deselect specific routes
|
||||||
rpc DeselectRoutes(SelectRoutesRequest) returns (SelectRoutesResponse) {}
|
rpc DeselectNetworks(SelectNetworksRequest) returns (SelectNetworksResponse) {}
|
||||||
|
|
||||||
// DebugBundle creates a debug bundle
|
// DebugBundle creates a debug bundle
|
||||||
rpc DebugBundle(DebugBundleRequest) returns (DebugBundleResponse) {}
|
rpc DebugBundle(DebugBundleRequest) returns (DebugBundleResponse) {}
|
||||||
@@ -45,7 +45,20 @@ service DaemonService {
|
|||||||
|
|
||||||
// SetLogLevel sets the log level of the daemon
|
// SetLogLevel sets the log level of the daemon
|
||||||
rpc SetLogLevel(SetLogLevelRequest) returns (SetLogLevelResponse) {}
|
rpc SetLogLevel(SetLogLevelRequest) returns (SetLogLevelResponse) {}
|
||||||
};
|
|
||||||
|
// List all states
|
||||||
|
rpc ListStates(ListStatesRequest) returns (ListStatesResponse) {}
|
||||||
|
|
||||||
|
// Clean specific state or all states
|
||||||
|
rpc CleanState(CleanStateRequest) returns (CleanStateResponse) {}
|
||||||
|
|
||||||
|
// Delete specific state or all states
|
||||||
|
rpc DeleteState(DeleteStateRequest) returns (DeleteStateResponse) {}
|
||||||
|
|
||||||
|
// SetNetworkMapPersistence enables or disables network map persistence
|
||||||
|
rpc SetNetworkMapPersistence(SetNetworkMapPersistenceRequest) returns (SetNetworkMapPersistenceResponse) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
message LoginRequest {
|
message LoginRequest {
|
||||||
// setupKey wiretrustee setup key.
|
// setupKey wiretrustee setup key.
|
||||||
@@ -177,7 +190,7 @@ message PeerState {
|
|||||||
int64 bytesRx = 13;
|
int64 bytesRx = 13;
|
||||||
int64 bytesTx = 14;
|
int64 bytesTx = 14;
|
||||||
bool rosenpassEnabled = 15;
|
bool rosenpassEnabled = 15;
|
||||||
repeated string routes = 16;
|
repeated string networks = 16;
|
||||||
google.protobuf.Duration latency = 17;
|
google.protobuf.Duration latency = 17;
|
||||||
string relayAddress = 18;
|
string relayAddress = 18;
|
||||||
}
|
}
|
||||||
@@ -190,7 +203,7 @@ message LocalPeerState {
|
|||||||
string fqdn = 4;
|
string fqdn = 4;
|
||||||
bool rosenpassEnabled = 5;
|
bool rosenpassEnabled = 5;
|
||||||
bool rosenpassPermissive = 6;
|
bool rosenpassPermissive = 6;
|
||||||
repeated string routes = 7;
|
repeated string networks = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignalState contains the latest state of a signal connection
|
// SignalState contains the latest state of a signal connection
|
||||||
@@ -231,20 +244,20 @@ message FullStatus {
|
|||||||
repeated NSGroupState dns_servers = 6;
|
repeated NSGroupState dns_servers = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListRoutesRequest {
|
message ListNetworksRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListRoutesResponse {
|
message ListNetworksResponse {
|
||||||
repeated Route routes = 1;
|
repeated Network routes = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SelectRoutesRequest {
|
message SelectNetworksRequest {
|
||||||
repeated string routeIDs = 1;
|
repeated string networkIDs = 1;
|
||||||
bool append = 2;
|
bool append = 2;
|
||||||
bool all = 3;
|
bool all = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SelectRoutesResponse {
|
message SelectNetworksResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
message IPList {
|
message IPList {
|
||||||
@@ -252,9 +265,9 @@ message IPList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
message Route {
|
message Network {
|
||||||
string ID = 1;
|
string ID = 1;
|
||||||
string network = 2;
|
string range = 2;
|
||||||
bool selected = 3;
|
bool selected = 3;
|
||||||
repeated string domains = 4;
|
repeated string domains = 4;
|
||||||
map<string, IPList> resolvedIPs = 5;
|
map<string, IPList> resolvedIPs = 5;
|
||||||
@@ -294,3 +307,45 @@ message SetLogLevelRequest {
|
|||||||
|
|
||||||
message SetLogLevelResponse {
|
message SetLogLevelResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// State represents a daemon state entry
|
||||||
|
message State {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListStatesRequest is empty as it requires no parameters
|
||||||
|
message ListStatesRequest {}
|
||||||
|
|
||||||
|
// ListStatesResponse contains a list of states
|
||||||
|
message ListStatesResponse {
|
||||||
|
repeated State states = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanStateRequest for cleaning states
|
||||||
|
message CleanStateRequest {
|
||||||
|
string state_name = 1;
|
||||||
|
bool all = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanStateResponse contains the result of the clean operation
|
||||||
|
message CleanStateResponse {
|
||||||
|
int32 cleaned_states = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteStateRequest for deleting states
|
||||||
|
message DeleteStateRequest {
|
||||||
|
string state_name = 1;
|
||||||
|
bool all = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteStateResponse contains the result of the delete operation
|
||||||
|
message DeleteStateResponse {
|
||||||
|
int32 deleted_states = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message SetNetworkMapPersistenceRequest {
|
||||||
|
bool enabled = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetNetworkMapPersistenceResponse {}
|
||||||
|
|||||||
@@ -31,18 +31,26 @@ type DaemonServiceClient interface {
|
|||||||
Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error)
|
Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error)
|
||||||
// GetConfig of the daemon.
|
// GetConfig of the daemon.
|
||||||
GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error)
|
GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error)
|
||||||
// List available network routes
|
// List available networks
|
||||||
ListRoutes(ctx context.Context, in *ListRoutesRequest, opts ...grpc.CallOption) (*ListRoutesResponse, error)
|
ListNetworks(ctx context.Context, in *ListNetworksRequest, opts ...grpc.CallOption) (*ListNetworksResponse, error)
|
||||||
// Select specific routes
|
// Select specific routes
|
||||||
SelectRoutes(ctx context.Context, in *SelectRoutesRequest, opts ...grpc.CallOption) (*SelectRoutesResponse, error)
|
SelectNetworks(ctx context.Context, in *SelectNetworksRequest, opts ...grpc.CallOption) (*SelectNetworksResponse, error)
|
||||||
// Deselect specific routes
|
// Deselect specific routes
|
||||||
DeselectRoutes(ctx context.Context, in *SelectRoutesRequest, opts ...grpc.CallOption) (*SelectRoutesResponse, error)
|
DeselectNetworks(ctx context.Context, in *SelectNetworksRequest, opts ...grpc.CallOption) (*SelectNetworksResponse, error)
|
||||||
// DebugBundle creates a debug bundle
|
// DebugBundle creates a debug bundle
|
||||||
DebugBundle(ctx context.Context, in *DebugBundleRequest, opts ...grpc.CallOption) (*DebugBundleResponse, error)
|
DebugBundle(ctx context.Context, in *DebugBundleRequest, opts ...grpc.CallOption) (*DebugBundleResponse, error)
|
||||||
// GetLogLevel gets the log level of the daemon
|
// GetLogLevel gets the log level of the daemon
|
||||||
GetLogLevel(ctx context.Context, in *GetLogLevelRequest, opts ...grpc.CallOption) (*GetLogLevelResponse, error)
|
GetLogLevel(ctx context.Context, in *GetLogLevelRequest, opts ...grpc.CallOption) (*GetLogLevelResponse, error)
|
||||||
// SetLogLevel sets the log level of the daemon
|
// SetLogLevel sets the log level of the daemon
|
||||||
SetLogLevel(ctx context.Context, in *SetLogLevelRequest, opts ...grpc.CallOption) (*SetLogLevelResponse, error)
|
SetLogLevel(ctx context.Context, in *SetLogLevelRequest, opts ...grpc.CallOption) (*SetLogLevelResponse, error)
|
||||||
|
// List all states
|
||||||
|
ListStates(ctx context.Context, in *ListStatesRequest, opts ...grpc.CallOption) (*ListStatesResponse, error)
|
||||||
|
// Clean specific state or all states
|
||||||
|
CleanState(ctx context.Context, in *CleanStateRequest, opts ...grpc.CallOption) (*CleanStateResponse, error)
|
||||||
|
// Delete specific state or all states
|
||||||
|
DeleteState(ctx context.Context, in *DeleteStateRequest, opts ...grpc.CallOption) (*DeleteStateResponse, error)
|
||||||
|
// SetNetworkMapPersistence enables or disables network map persistence
|
||||||
|
SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type daemonServiceClient struct {
|
type daemonServiceClient struct {
|
||||||
@@ -107,27 +115,27 @@ func (c *daemonServiceClient) GetConfig(ctx context.Context, in *GetConfigReques
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *daemonServiceClient) ListRoutes(ctx context.Context, in *ListRoutesRequest, opts ...grpc.CallOption) (*ListRoutesResponse, error) {
|
func (c *daemonServiceClient) ListNetworks(ctx context.Context, in *ListNetworksRequest, opts ...grpc.CallOption) (*ListNetworksResponse, error) {
|
||||||
out := new(ListRoutesResponse)
|
out := new(ListNetworksResponse)
|
||||||
err := c.cc.Invoke(ctx, "/daemon.DaemonService/ListRoutes", in, out, opts...)
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/ListNetworks", in, out, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *daemonServiceClient) SelectRoutes(ctx context.Context, in *SelectRoutesRequest, opts ...grpc.CallOption) (*SelectRoutesResponse, error) {
|
func (c *daemonServiceClient) SelectNetworks(ctx context.Context, in *SelectNetworksRequest, opts ...grpc.CallOption) (*SelectNetworksResponse, error) {
|
||||||
out := new(SelectRoutesResponse)
|
out := new(SelectNetworksResponse)
|
||||||
err := c.cc.Invoke(ctx, "/daemon.DaemonService/SelectRoutes", in, out, opts...)
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/SelectNetworks", in, out, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *daemonServiceClient) DeselectRoutes(ctx context.Context, in *SelectRoutesRequest, opts ...grpc.CallOption) (*SelectRoutesResponse, error) {
|
func (c *daemonServiceClient) DeselectNetworks(ctx context.Context, in *SelectNetworksRequest, opts ...grpc.CallOption) (*SelectNetworksResponse, error) {
|
||||||
out := new(SelectRoutesResponse)
|
out := new(SelectNetworksResponse)
|
||||||
err := c.cc.Invoke(ctx, "/daemon.DaemonService/DeselectRoutes", in, out, opts...)
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/DeselectNetworks", in, out, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -161,6 +169,42 @@ func (c *daemonServiceClient) SetLogLevel(ctx context.Context, in *SetLogLevelRe
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) ListStates(ctx context.Context, in *ListStatesRequest, opts ...grpc.CallOption) (*ListStatesResponse, error) {
|
||||||
|
out := new(ListStatesResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/ListStates", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) CleanState(ctx context.Context, in *CleanStateRequest, opts ...grpc.CallOption) (*CleanStateResponse, error) {
|
||||||
|
out := new(CleanStateResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/CleanState", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) DeleteState(ctx context.Context, in *DeleteStateRequest, opts ...grpc.CallOption) (*DeleteStateResponse, error) {
|
||||||
|
out := new(DeleteStateResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/DeleteState", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) {
|
||||||
|
out := new(SetNetworkMapPersistenceResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/SetNetworkMapPersistence", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, 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
|
||||||
@@ -178,18 +222,26 @@ type DaemonServiceServer interface {
|
|||||||
Down(context.Context, *DownRequest) (*DownResponse, error)
|
Down(context.Context, *DownRequest) (*DownResponse, error)
|
||||||
// GetConfig of the daemon.
|
// GetConfig of the daemon.
|
||||||
GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error)
|
GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error)
|
||||||
// List available network routes
|
// List available networks
|
||||||
ListRoutes(context.Context, *ListRoutesRequest) (*ListRoutesResponse, error)
|
ListNetworks(context.Context, *ListNetworksRequest) (*ListNetworksResponse, error)
|
||||||
// Select specific routes
|
// Select specific routes
|
||||||
SelectRoutes(context.Context, *SelectRoutesRequest) (*SelectRoutesResponse, error)
|
SelectNetworks(context.Context, *SelectNetworksRequest) (*SelectNetworksResponse, error)
|
||||||
// Deselect specific routes
|
// Deselect specific routes
|
||||||
DeselectRoutes(context.Context, *SelectRoutesRequest) (*SelectRoutesResponse, error)
|
DeselectNetworks(context.Context, *SelectNetworksRequest) (*SelectNetworksResponse, error)
|
||||||
// DebugBundle creates a debug bundle
|
// DebugBundle creates a debug bundle
|
||||||
DebugBundle(context.Context, *DebugBundleRequest) (*DebugBundleResponse, error)
|
DebugBundle(context.Context, *DebugBundleRequest) (*DebugBundleResponse, error)
|
||||||
// GetLogLevel gets the log level of the daemon
|
// GetLogLevel gets the log level of the daemon
|
||||||
GetLogLevel(context.Context, *GetLogLevelRequest) (*GetLogLevelResponse, error)
|
GetLogLevel(context.Context, *GetLogLevelRequest) (*GetLogLevelResponse, error)
|
||||||
// SetLogLevel sets the log level of the daemon
|
// SetLogLevel sets the log level of the daemon
|
||||||
SetLogLevel(context.Context, *SetLogLevelRequest) (*SetLogLevelResponse, error)
|
SetLogLevel(context.Context, *SetLogLevelRequest) (*SetLogLevelResponse, error)
|
||||||
|
// List all states
|
||||||
|
ListStates(context.Context, *ListStatesRequest) (*ListStatesResponse, error)
|
||||||
|
// Clean specific state or all states
|
||||||
|
CleanState(context.Context, *CleanStateRequest) (*CleanStateResponse, error)
|
||||||
|
// Delete specific state or all states
|
||||||
|
DeleteState(context.Context, *DeleteStateRequest) (*DeleteStateResponse, error)
|
||||||
|
// SetNetworkMapPersistence enables or disables network map persistence
|
||||||
|
SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error)
|
||||||
mustEmbedUnimplementedDaemonServiceServer()
|
mustEmbedUnimplementedDaemonServiceServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,14 +267,14 @@ func (UnimplementedDaemonServiceServer) Down(context.Context, *DownRequest) (*Do
|
|||||||
func (UnimplementedDaemonServiceServer) GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) {
|
func (UnimplementedDaemonServiceServer) GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedDaemonServiceServer) ListRoutes(context.Context, *ListRoutesRequest) (*ListRoutesResponse, error) {
|
func (UnimplementedDaemonServiceServer) ListNetworks(context.Context, *ListNetworksRequest) (*ListNetworksResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoutes not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ListNetworks not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedDaemonServiceServer) SelectRoutes(context.Context, *SelectRoutesRequest) (*SelectRoutesResponse, error) {
|
func (UnimplementedDaemonServiceServer) SelectNetworks(context.Context, *SelectNetworksRequest) (*SelectNetworksResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method SelectRoutes not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method SelectNetworks not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedDaemonServiceServer) DeselectRoutes(context.Context, *SelectRoutesRequest) (*SelectRoutesResponse, error) {
|
func (UnimplementedDaemonServiceServer) DeselectNetworks(context.Context, *SelectNetworksRequest) (*SelectNetworksResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method DeselectRoutes not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method DeselectNetworks not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedDaemonServiceServer) DebugBundle(context.Context, *DebugBundleRequest) (*DebugBundleResponse, error) {
|
func (UnimplementedDaemonServiceServer) DebugBundle(context.Context, *DebugBundleRequest) (*DebugBundleResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method DebugBundle not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method DebugBundle not implemented")
|
||||||
@@ -233,6 +285,18 @@ func (UnimplementedDaemonServiceServer) GetLogLevel(context.Context, *GetLogLeve
|
|||||||
func (UnimplementedDaemonServiceServer) SetLogLevel(context.Context, *SetLogLevelRequest) (*SetLogLevelResponse, error) {
|
func (UnimplementedDaemonServiceServer) SetLogLevel(context.Context, *SetLogLevelRequest) (*SetLogLevelResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method SetLogLevel not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method SetLogLevel not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) ListStates(context.Context, *ListStatesRequest) (*ListStatesResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListStates not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) CleanState(context.Context, *CleanStateRequest) (*CleanStateResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CleanState not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) DeleteState(context.Context, *DeleteStateRequest) (*DeleteStateResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method DeleteState not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method SetNetworkMapPersistence 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.
|
||||||
@@ -354,56 +418,56 @@ func _DaemonService_GetConfig_Handler(srv interface{}, ctx context.Context, dec
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _DaemonService_ListRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _DaemonService_ListNetworks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ListRoutesRequest)
|
in := new(ListNetworksRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if interceptor == nil {
|
if interceptor == nil {
|
||||||
return srv.(DaemonServiceServer).ListRoutes(ctx, in)
|
return srv.(DaemonServiceServer).ListNetworks(ctx, in)
|
||||||
}
|
}
|
||||||
info := &grpc.UnaryServerInfo{
|
info := &grpc.UnaryServerInfo{
|
||||||
Server: srv,
|
Server: srv,
|
||||||
FullMethod: "/daemon.DaemonService/ListRoutes",
|
FullMethod: "/daemon.DaemonService/ListNetworks",
|
||||||
}
|
}
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
return srv.(DaemonServiceServer).ListRoutes(ctx, req.(*ListRoutesRequest))
|
return srv.(DaemonServiceServer).ListNetworks(ctx, req.(*ListNetworksRequest))
|
||||||
}
|
}
|
||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _DaemonService_SelectRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _DaemonService_SelectNetworks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(SelectRoutesRequest)
|
in := new(SelectNetworksRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if interceptor == nil {
|
if interceptor == nil {
|
||||||
return srv.(DaemonServiceServer).SelectRoutes(ctx, in)
|
return srv.(DaemonServiceServer).SelectNetworks(ctx, in)
|
||||||
}
|
}
|
||||||
info := &grpc.UnaryServerInfo{
|
info := &grpc.UnaryServerInfo{
|
||||||
Server: srv,
|
Server: srv,
|
||||||
FullMethod: "/daemon.DaemonService/SelectRoutes",
|
FullMethod: "/daemon.DaemonService/SelectNetworks",
|
||||||
}
|
}
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
return srv.(DaemonServiceServer).SelectRoutes(ctx, req.(*SelectRoutesRequest))
|
return srv.(DaemonServiceServer).SelectNetworks(ctx, req.(*SelectNetworksRequest))
|
||||||
}
|
}
|
||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _DaemonService_DeselectRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _DaemonService_DeselectNetworks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(SelectRoutesRequest)
|
in := new(SelectNetworksRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if interceptor == nil {
|
if interceptor == nil {
|
||||||
return srv.(DaemonServiceServer).DeselectRoutes(ctx, in)
|
return srv.(DaemonServiceServer).DeselectNetworks(ctx, in)
|
||||||
}
|
}
|
||||||
info := &grpc.UnaryServerInfo{
|
info := &grpc.UnaryServerInfo{
|
||||||
Server: srv,
|
Server: srv,
|
||||||
FullMethod: "/daemon.DaemonService/DeselectRoutes",
|
FullMethod: "/daemon.DaemonService/DeselectNetworks",
|
||||||
}
|
}
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
return srv.(DaemonServiceServer).DeselectRoutes(ctx, req.(*SelectRoutesRequest))
|
return srv.(DaemonServiceServer).DeselectNetworks(ctx, req.(*SelectNetworksRequest))
|
||||||
}
|
}
|
||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
@@ -462,6 +526,78 @@ func _DaemonService_SetLogLevel_Handler(srv interface{}, ctx context.Context, de
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _DaemonService_ListStates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListStatesRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DaemonServiceServer).ListStates(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/daemon.DaemonService/ListStates",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DaemonServiceServer).ListStates(ctx, req.(*ListStatesRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _DaemonService_CleanState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CleanStateRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DaemonServiceServer).CleanState(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/daemon.DaemonService/CleanState",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DaemonServiceServer).CleanState(ctx, req.(*CleanStateRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _DaemonService_DeleteState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(DeleteStateRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DaemonServiceServer).DeleteState(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/daemon.DaemonService/DeleteState",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DaemonServiceServer).DeleteState(ctx, req.(*DeleteStateRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _DaemonService_SetNetworkMapPersistence_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(SetNetworkMapPersistenceRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DaemonServiceServer).SetNetworkMapPersistence(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/daemon.DaemonService/SetNetworkMapPersistence",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DaemonServiceServer).SetNetworkMapPersistence(ctx, req.(*SetNetworkMapPersistenceRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -494,16 +630,16 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
Handler: _DaemonService_GetConfig_Handler,
|
Handler: _DaemonService_GetConfig_Handler,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
MethodName: "ListRoutes",
|
MethodName: "ListNetworks",
|
||||||
Handler: _DaemonService_ListRoutes_Handler,
|
Handler: _DaemonService_ListNetworks_Handler,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
MethodName: "SelectRoutes",
|
MethodName: "SelectNetworks",
|
||||||
Handler: _DaemonService_SelectRoutes_Handler,
|
Handler: _DaemonService_SelectNetworks_Handler,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
MethodName: "DeselectRoutes",
|
MethodName: "DeselectNetworks",
|
||||||
Handler: _DaemonService_DeselectRoutes_Handler,
|
Handler: _DaemonService_DeselectNetworks_Handler,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
MethodName: "DebugBundle",
|
MethodName: "DebugBundle",
|
||||||
@@ -517,6 +653,22 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "SetLogLevel",
|
MethodName: "SetLogLevel",
|
||||||
Handler: _DaemonService_SetLogLevel_Handler,
|
Handler: _DaemonService_SetLogLevel_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListStates",
|
||||||
|
Handler: _DaemonService_ListStates_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CleanState",
|
||||||
|
Handler: _DaemonService_CleanState_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "DeleteState",
|
||||||
|
Handler: _DaemonService_DeleteState_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "SetNetworkMapPersistence",
|
||||||
|
Handler: _DaemonService_SetNetworkMapPersistence_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "daemon.proto",
|
Metadata: "daemon.proto",
|
||||||
|
|||||||
@@ -5,32 +5,44 @@ package server
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/anonymize"
|
"github.com/netbirdio/netbird/client/anonymize"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const readmeContent = `Netbird debug bundle
|
const readmeContent = `Netbird debug bundle
|
||||||
This debug bundle contains the following files:
|
This debug bundle contains the following files:
|
||||||
|
|
||||||
status.txt: Anonymized status information of the NetBird client.
|
status.txt: Anonymized status information of the NetBird client.
|
||||||
client.log: Most recent, anonymized log file of the NetBird client.
|
client.log: Most recent, anonymized client log file of the NetBird client.
|
||||||
|
netbird.err: Most recent, anonymized stderr log file of the NetBird client.
|
||||||
|
netbird.out: Most recent, anonymized stdout log file of the NetBird client.
|
||||||
routes.txt: Anonymized system routes, if --system-info flag was provided.
|
routes.txt: Anonymized system routes, if --system-info flag was provided.
|
||||||
interfaces.txt: Anonymized network interface information, if --system-info flag was provided.
|
interfaces.txt: Anonymized network interface information, if --system-info flag was provided.
|
||||||
config.txt: Anonymized configuration information of the NetBird client.
|
config.txt: Anonymized configuration information of the NetBird client.
|
||||||
|
network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules.
|
||||||
|
state.json: Anonymized client state dump containing netbird states.
|
||||||
|
|
||||||
|
|
||||||
Anonymization Process
|
Anonymization Process
|
||||||
@@ -50,8 +62,32 @@ Domains
|
|||||||
All domain names (except for the netbird domains) are replaced with randomly generated strings ending in ".domain". Anonymized domains are consistent across all files in the bundle.
|
All domain names (except for the netbird domains) are replaced with randomly generated strings ending in ".domain". Anonymized domains are consistent across all files in the bundle.
|
||||||
Reoccuring domain names are replaced with the same anonymized domain.
|
Reoccuring domain names are replaced with the same anonymized domain.
|
||||||
|
|
||||||
|
Network Map
|
||||||
|
The network_map.json file contains the following anonymized information:
|
||||||
|
- Peer configurations (addresses, FQDNs, DNS settings)
|
||||||
|
- Remote and offline peer information (allowed IPs, FQDNs)
|
||||||
|
- Routes (network ranges, associated domains)
|
||||||
|
- DNS configuration (nameservers, domains, custom zones)
|
||||||
|
- Firewall rules (peer IPs, source/destination ranges)
|
||||||
|
|
||||||
|
SSH keys in the network map are replaced with a placeholder value. All IP addresses and domains in the network map follow the same anonymization rules as described above.
|
||||||
|
|
||||||
|
State File
|
||||||
|
The state.json file contains anonymized internal state information of the NetBird client, including:
|
||||||
|
- DNS settings and configuration
|
||||||
|
- Firewall rules
|
||||||
|
- Exclusion routes
|
||||||
|
- Route selection
|
||||||
|
- Other internal states that may be present
|
||||||
|
|
||||||
|
The state file follows the same anonymization rules as other files:
|
||||||
|
- IP addresses (both individual and CIDR ranges) are anonymized while preserving their structure
|
||||||
|
- Domain names are consistently anonymized
|
||||||
|
- Technical identifiers and non-sensitive data remain unchanged
|
||||||
|
|
||||||
Routes
|
Routes
|
||||||
For anonymized routes, the IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct.
|
For anonymized routes, the IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct.
|
||||||
|
|
||||||
Network Interfaces
|
Network Interfaces
|
||||||
The interfaces.txt file contains information about network interfaces, including:
|
The interfaces.txt file contains information about network interfaces, including:
|
||||||
- Interface name
|
- Interface name
|
||||||
@@ -72,6 +108,12 @@ The config.txt file contains anonymized configuration information of the NetBird
|
|||||||
Other non-sensitive configuration options are included without anonymization.
|
Other non-sensitive configuration options are included without anonymization.
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const (
|
||||||
|
clientLogFile = "client.log"
|
||||||
|
errorLogFile = "netbird.err"
|
||||||
|
stdoutLogFile = "netbird.out"
|
||||||
|
)
|
||||||
|
|
||||||
// DebugBundle creates a debug bundle and returns the location.
|
// DebugBundle creates a debug bundle and returns the location.
|
||||||
func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) (resp *proto.DebugBundleResponse, err error) {
|
func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) (resp *proto.DebugBundleResponse, err error) {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
@@ -119,19 +161,27 @@ func (s *Server) createArchive(bundlePath *os.File, req *proto.DebugBundleReques
|
|||||||
seedFromStatus(anonymizer, &status)
|
seedFromStatus(anonymizer, &status)
|
||||||
|
|
||||||
if err := s.addConfig(req, anonymizer, archive); err != nil {
|
if err := s.addConfig(req, anonymizer, archive); err != nil {
|
||||||
return fmt.Errorf("add config: %w", err)
|
log.Errorf("Failed to add config to debug bundle: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.GetSystemInfo() {
|
if req.GetSystemInfo() {
|
||||||
if err := s.addRoutes(req, anonymizer, archive); err != nil {
|
if err := s.addRoutes(req, anonymizer, archive); err != nil {
|
||||||
return fmt.Errorf("add routes: %w", err)
|
log.Errorf("Failed to add routes to debug bundle: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.addInterfaces(req, anonymizer, archive); err != nil {
|
if err := s.addInterfaces(req, anonymizer, archive); err != nil {
|
||||||
return fmt.Errorf("add interfaces: %w", err)
|
log.Errorf("Failed to add interfaces to debug bundle: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.addNetworkMap(req, anonymizer, archive); err != nil {
|
||||||
|
return fmt.Errorf("add network map: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.addStateFile(req, anonymizer, archive); err != nil {
|
||||||
|
log.Errorf("Failed to add state file to debug bundle: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.addLogfile(req, anonymizer, archive); err != nil {
|
if err := s.addLogfile(req, anonymizer, archive); err != nil {
|
||||||
return fmt.Errorf("add log file: %w", err)
|
return fmt.Errorf("add log file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -220,15 +270,16 @@ func (s *Server) addCommonConfigFields(configContent *strings.Builder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) addRoutes(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
|
func (s *Server) addRoutes(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
|
||||||
if routes, err := systemops.GetRoutesFromTable(); err != nil {
|
routes, err := systemops.GetRoutesFromTable()
|
||||||
log.Errorf("Failed to get routes: %v", err)
|
if err != nil {
|
||||||
} else {
|
return fmt.Errorf("get routes: %w", err)
|
||||||
// TODO: get routes including nexthop
|
}
|
||||||
routesContent := formatRoutes(routes, req.GetAnonymize(), anonymizer)
|
|
||||||
routesReader := strings.NewReader(routesContent)
|
// TODO: get routes including nexthop
|
||||||
if err := addFileToZip(archive, routesReader, "routes.txt"); err != nil {
|
routesContent := formatRoutes(routes, req.GetAnonymize(), anonymizer)
|
||||||
return fmt.Errorf("add routes file to zip: %w", err)
|
routesReader := strings.NewReader(routesContent)
|
||||||
}
|
if err := addFileToZip(archive, routesReader, "routes.txt"); err != nil {
|
||||||
|
return fmt.Errorf("add routes file to zip: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -248,14 +299,106 @@ func (s *Server) addInterfaces(req *proto.DebugBundleRequest, anonymizer *anonym
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) (err error) {
|
func (s *Server) addNetworkMap(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
|
||||||
logFile, err := os.Open(s.logFile)
|
networkMap, err := s.getLatestNetworkMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open log file: %w", err)
|
// Skip if network map is not available, but log it
|
||||||
|
log.Debugf("skipping empty network map in debug bundle: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.GetAnonymize() {
|
||||||
|
if err := anonymizeNetworkMap(networkMap, anonymizer); err != nil {
|
||||||
|
return fmt.Errorf("anonymize network map: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options := protojson.MarshalOptions{
|
||||||
|
EmitUnpopulated: true,
|
||||||
|
UseProtoNames: true,
|
||||||
|
Indent: " ",
|
||||||
|
AllowPartial: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes, err := options.Marshal(networkMap)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("generate json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := addFileToZip(archive, bytes.NewReader(jsonBytes), "network_map.json"); err != nil {
|
||||||
|
return fmt.Errorf("add network map to zip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) addStateFile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
|
||||||
|
path := statemanager.GetDefaultStatePath()
|
||||||
|
if path == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("read state file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.GetAnonymize() {
|
||||||
|
var rawStates map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &rawStates); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal states: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := anonymizeStateFile(&rawStates, anonymizer); err != nil {
|
||||||
|
return fmt.Errorf("anonymize state file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bs, err := json.MarshalIndent(rawStates, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal states: %w", err)
|
||||||
|
}
|
||||||
|
data = bs
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := addFileToZip(archive, bytes.NewReader(data), "state.json"); err != nil {
|
||||||
|
return fmt.Errorf("add state file to zip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
|
||||||
|
logDir := filepath.Dir(s.logFile)
|
||||||
|
|
||||||
|
if err := s.addSingleLogfile(s.logFile, clientLogFile, req, anonymizer, archive); err != nil {
|
||||||
|
return fmt.Errorf("add client log file to zip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
errLogPath := filepath.Join(logDir, errorLogFile)
|
||||||
|
if err := s.addSingleLogfile(errLogPath, errorLogFile, req, anonymizer, archive); err != nil {
|
||||||
|
log.Warnf("Failed to add %s to zip: %v", errorLogFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdoutLogPath := filepath.Join(logDir, stdoutLogFile)
|
||||||
|
if err := s.addSingleLogfile(stdoutLogPath, stdoutLogFile, req, anonymizer, archive); err != nil {
|
||||||
|
log.Warnf("Failed to add %s to zip: %v", stdoutLogFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addSingleLogfile adds a single log file to the archive
|
||||||
|
func (s *Server) addSingleLogfile(logPath, targetName string, req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
|
||||||
|
logFile, err := os.Open(logPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open log file %s: %w", targetName, err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := logFile.Close(); err != nil {
|
if err := logFile.Close(); err != nil {
|
||||||
log.Errorf("Failed to close original log file: %v", err)
|
log.Errorf("Failed to close log file %s: %v", targetName, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -264,45 +407,55 @@ func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize
|
|||||||
var writer *io.PipeWriter
|
var writer *io.PipeWriter
|
||||||
logReader, writer = io.Pipe()
|
logReader, writer = io.Pipe()
|
||||||
|
|
||||||
go s.anonymize(logFile, writer, anonymizer)
|
go anonymizeLog(logFile, writer, anonymizer)
|
||||||
} else {
|
} else {
|
||||||
logReader = logFile
|
logReader = logFile
|
||||||
}
|
}
|
||||||
if err := addFileToZip(archive, logReader, "client.log"); err != nil {
|
|
||||||
return fmt.Errorf("add log file to zip: %w", err)
|
if err := addFileToZip(archive, logReader, targetName); err != nil {
|
||||||
|
return fmt.Errorf("add %s to zip: %w", targetName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) anonymize(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) {
|
// getLatestNetworkMap returns the latest network map from the engine if network map persistence is enabled
|
||||||
defer func() {
|
func (s *Server) getLatestNetworkMap() (*mgmProto.NetworkMap, error) {
|
||||||
// always nil
|
if s.connectClient == nil {
|
||||||
_ = writer.Close()
|
return nil, errors.New("connect client is not initialized")
|
||||||
}()
|
}
|
||||||
|
|
||||||
scanner := bufio.NewScanner(reader)
|
engine := s.connectClient.Engine()
|
||||||
for scanner.Scan() {
|
if engine == nil {
|
||||||
line := anonymizer.AnonymizeString(scanner.Text())
|
return nil, errors.New("engine is not initialized")
|
||||||
if _, err := writer.Write([]byte(line + "\n")); err != nil {
|
|
||||||
writer.CloseWithError(fmt.Errorf("anonymize write: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
writer.CloseWithError(fmt.Errorf("anonymize scan: %w", err))
|
networkMap, err := engine.GetLatestNetworkMap()
|
||||||
return
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get latest network map: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if networkMap == nil {
|
||||||
|
return nil, errors.New("network map is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
return networkMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLogLevel gets the current logging level for the server.
|
// GetLogLevel gets the current logging level for the server.
|
||||||
func (s *Server) GetLogLevel(_ context.Context, _ *proto.GetLogLevelRequest) (*proto.GetLogLevelResponse, error) {
|
func (s *Server) GetLogLevel(_ context.Context, _ *proto.GetLogLevelRequest) (*proto.GetLogLevelResponse, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
level := ParseLogLevel(log.GetLevel().String())
|
level := ParseLogLevel(log.GetLevel().String())
|
||||||
return &proto.GetLogLevelResponse{Level: level}, nil
|
return &proto.GetLogLevelResponse{Level: level}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLogLevel sets the logging level for the server.
|
// SetLogLevel sets the logging level for the server.
|
||||||
func (s *Server) SetLogLevel(_ context.Context, req *proto.SetLogLevelRequest) (*proto.SetLogLevelResponse, error) {
|
func (s *Server) SetLogLevel(_ context.Context, req *proto.SetLogLevelRequest) (*proto.SetLogLevelResponse, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
level, err := log.ParseLevel(req.Level.String())
|
level, err := log.ParseLevel(req.Level.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid log level: %w", err)
|
return nil, fmt.Errorf("invalid log level: %w", err)
|
||||||
@@ -313,6 +466,20 @@ func (s *Server) SetLogLevel(_ context.Context, req *proto.SetLogLevelRequest) (
|
|||||||
return &proto.SetLogLevelResponse{}, nil
|
return &proto.SetLogLevelResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetNetworkMapPersistence sets the network map persistence for the server.
|
||||||
|
func (s *Server) SetNetworkMapPersistence(_ context.Context, req *proto.SetNetworkMapPersistenceRequest) (*proto.SetNetworkMapPersistenceResponse, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
enabled := req.GetEnabled()
|
||||||
|
s.persistNetworkMap = enabled
|
||||||
|
if s.connectClient != nil {
|
||||||
|
s.connectClient.SetNetworkMapPersistence(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &proto.SetNetworkMapPersistenceResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func addFileToZip(archive *zip.Writer, reader io.Reader, filename string) error {
|
func addFileToZip(archive *zip.Writer, reader io.Reader, filename string) error {
|
||||||
header := &zip.FileHeader{
|
header := &zip.FileHeader{
|
||||||
Name: filename,
|
Name: filename,
|
||||||
@@ -458,6 +625,26 @@ func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *an
|
|||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func anonymizeLog(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) {
|
||||||
|
defer func() {
|
||||||
|
// always nil
|
||||||
|
_ = writer.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := anonymizer.AnonymizeString(scanner.Text())
|
||||||
|
if _, err := writer.Write([]byte(line + "\n")); err != nil {
|
||||||
|
writer.CloseWithError(fmt.Errorf("anonymize write: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
writer.CloseWithError(fmt.Errorf("anonymize scan: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func anonymizeNATExternalIPs(ips []string, anonymizer *anonymize.Anonymizer) []string {
|
func anonymizeNATExternalIPs(ips []string, anonymizer *anonymize.Anonymizer) []string {
|
||||||
anonymizedIPs := make([]string, len(ips))
|
anonymizedIPs := make([]string, len(ips))
|
||||||
for i, ip := range ips {
|
for i, ip := range ips {
|
||||||
@@ -484,3 +671,248 @@ func anonymizeNATExternalIPs(ips []string, anonymizer *anonymize.Anonymizer) []s
|
|||||||
}
|
}
|
||||||
return anonymizedIPs
|
return anonymizedIPs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func anonymizeNetworkMap(networkMap *mgmProto.NetworkMap, anonymizer *anonymize.Anonymizer) error {
|
||||||
|
if networkMap.PeerConfig != nil {
|
||||||
|
anonymizePeerConfig(networkMap.PeerConfig, anonymizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range networkMap.RemotePeers {
|
||||||
|
anonymizeRemotePeer(peer, anonymizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range networkMap.OfflinePeers {
|
||||||
|
anonymizeRemotePeer(peer, anonymizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range networkMap.Routes {
|
||||||
|
anonymizeRoute(r, anonymizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if networkMap.DNSConfig != nil {
|
||||||
|
anonymizeDNSConfig(networkMap.DNSConfig, anonymizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range networkMap.FirewallRules {
|
||||||
|
anonymizeFirewallRule(rule, anonymizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range networkMap.RoutesFirewallRules {
|
||||||
|
anonymizeRouteFirewallRule(rule, anonymizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizePeerConfig(config *mgmProto.PeerConfig, anonymizer *anonymize.Anonymizer) {
|
||||||
|
if config == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr, err := netip.ParseAddr(config.Address); err == nil {
|
||||||
|
config.Address = anonymizer.AnonymizeIP(addr).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.SshConfig != nil && len(config.SshConfig.SshPubKey) > 0 {
|
||||||
|
config.SshConfig.SshPubKey = []byte("ssh-placeholder-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Dns = anonymizer.AnonymizeString(config.Dns)
|
||||||
|
config.Fqdn = anonymizer.AnonymizeDomain(config.Fqdn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeRemotePeer(peer *mgmProto.RemotePeerConfig, anonymizer *anonymize.Anonymizer) {
|
||||||
|
if peer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, ip := range peer.AllowedIps {
|
||||||
|
// Try to parse as prefix first (CIDR)
|
||||||
|
if prefix, err := netip.ParsePrefix(ip); err == nil {
|
||||||
|
anonIP := anonymizer.AnonymizeIP(prefix.Addr())
|
||||||
|
peer.AllowedIps[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
|
||||||
|
} else if addr, err := netip.ParseAddr(ip); err == nil {
|
||||||
|
peer.AllowedIps[i] = anonymizer.AnonymizeIP(addr).String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.Fqdn = anonymizer.AnonymizeDomain(peer.Fqdn)
|
||||||
|
|
||||||
|
if peer.SshConfig != nil && len(peer.SshConfig.SshPubKey) > 0 {
|
||||||
|
peer.SshConfig.SshPubKey = []byte("ssh-placeholder-key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeRoute(route *mgmProto.Route, anonymizer *anonymize.Anonymizer) {
|
||||||
|
if route == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix, err := netip.ParsePrefix(route.Network); err == nil {
|
||||||
|
anonIP := anonymizer.AnonymizeIP(prefix.Addr())
|
||||||
|
route.Network = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, domain := range route.Domains {
|
||||||
|
route.Domains[i] = anonymizer.AnonymizeDomain(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
route.NetID = anonymizer.AnonymizeString(route.NetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeDNSConfig(config *mgmProto.DNSConfig, anonymizer *anonymize.Anonymizer) {
|
||||||
|
if config == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
anonymizeNameServerGroups(config.NameServerGroups, anonymizer)
|
||||||
|
anonymizeCustomZones(config.CustomZones, anonymizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeNameServerGroups(groups []*mgmProto.NameServerGroup, anonymizer *anonymize.Anonymizer) {
|
||||||
|
for _, group := range groups {
|
||||||
|
anonymizeServers(group.NameServers, anonymizer)
|
||||||
|
anonymizeDomains(group.Domains, anonymizer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeServers(servers []*mgmProto.NameServer, anonymizer *anonymize.Anonymizer) {
|
||||||
|
for _, server := range servers {
|
||||||
|
if addr, err := netip.ParseAddr(server.IP); err == nil {
|
||||||
|
server.IP = anonymizer.AnonymizeIP(addr).String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeDomains(domains []string, anonymizer *anonymize.Anonymizer) {
|
||||||
|
for i, domain := range domains {
|
||||||
|
domains[i] = anonymizer.AnonymizeDomain(domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeCustomZones(zones []*mgmProto.CustomZone, anonymizer *anonymize.Anonymizer) {
|
||||||
|
for _, zone := range zones {
|
||||||
|
zone.Domain = anonymizer.AnonymizeDomain(zone.Domain)
|
||||||
|
anonymizeRecords(zone.Records, anonymizer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeRecords(records []*mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) {
|
||||||
|
for _, record := range records {
|
||||||
|
record.Name = anonymizer.AnonymizeDomain(record.Name)
|
||||||
|
anonymizeRData(record, anonymizer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeRData(record *mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) {
|
||||||
|
switch record.Type {
|
||||||
|
case 1, 28: // A or AAAA record
|
||||||
|
if addr, err := netip.ParseAddr(record.RData); err == nil {
|
||||||
|
record.RData = anonymizer.AnonymizeIP(addr).String()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
record.RData = anonymizer.AnonymizeString(record.RData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeFirewallRule(rule *mgmProto.FirewallRule, anonymizer *anonymize.Anonymizer) {
|
||||||
|
if rule == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr, err := netip.ParseAddr(rule.PeerIP); err == nil {
|
||||||
|
rule.PeerIP = anonymizer.AnonymizeIP(addr).String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeRouteFirewallRule(rule *mgmProto.RouteFirewallRule, anonymizer *anonymize.Anonymizer) {
|
||||||
|
if rule == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, sourceRange := range rule.SourceRanges {
|
||||||
|
if prefix, err := netip.ParsePrefix(sourceRange); err == nil {
|
||||||
|
anonIP := anonymizer.AnonymizeIP(prefix.Addr())
|
||||||
|
rule.SourceRanges[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix, err := netip.ParsePrefix(rule.Destination); err == nil {
|
||||||
|
anonIP := anonymizer.AnonymizeIP(prefix.Addr())
|
||||||
|
rule.Destination = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeStateFile(rawStates *map[string]json.RawMessage, anonymizer *anonymize.Anonymizer) error {
|
||||||
|
for name, rawState := range *rawStates {
|
||||||
|
if string(rawState) == "null" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var state map[string]any
|
||||||
|
if err := json.Unmarshal(rawState, &state); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal state %s: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state = anonymizeValue(state, anonymizer).(map[string]any)
|
||||||
|
|
||||||
|
bs, err := json.Marshal(state)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal state %s: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
(*rawStates)[name] = bs
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeValue(value any, anonymizer *anonymize.Anonymizer) any {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
return anonymizeString(v, anonymizer)
|
||||||
|
case map[string]any:
|
||||||
|
return anonymizeMap(v, anonymizer)
|
||||||
|
case []any:
|
||||||
|
return anonymizeSlice(v, anonymizer)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeString(v string, anonymizer *anonymize.Anonymizer) string {
|
||||||
|
if prefix, err := netip.ParsePrefix(v); err == nil {
|
||||||
|
anonIP := anonymizer.AnonymizeIP(prefix.Addr())
|
||||||
|
return fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
|
||||||
|
}
|
||||||
|
if ip, err := netip.ParseAddr(v); err == nil {
|
||||||
|
return anonymizer.AnonymizeIP(ip).String()
|
||||||
|
}
|
||||||
|
return anonymizer.AnonymizeString(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeMap(v map[string]any, anonymizer *anonymize.Anonymizer) map[string]any {
|
||||||
|
result := make(map[string]any, len(v))
|
||||||
|
for key, val := range v {
|
||||||
|
newKey := anonymizeMapKey(key, anonymizer)
|
||||||
|
result[newKey] = anonymizeValue(val, anonymizer)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeMapKey(key string, anonymizer *anonymize.Anonymizer) string {
|
||||||
|
if prefix, err := netip.ParsePrefix(key); err == nil {
|
||||||
|
anonIP := anonymizer.AnonymizeIP(prefix.Addr())
|
||||||
|
return fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
|
||||||
|
}
|
||||||
|
if ip, err := netip.ParseAddr(key); err == nil {
|
||||||
|
return anonymizer.AnonymizeIP(ip).String()
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeSlice(v []any, anonymizer *anonymize.Anonymizer) []any {
|
||||||
|
for i, val := range v {
|
||||||
|
v[i] = anonymizeValue(val, anonymizer)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|||||||
430
client/server/debug_test.go
Normal file
430
client/server/debug_test.go
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/anonymize"
|
||||||
|
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAnonymizeStateFile(t *testing.T) {
|
||||||
|
testState := map[string]json.RawMessage{
|
||||||
|
"null_state": json.RawMessage("null"),
|
||||||
|
"test_state": mustMarshal(map[string]any{
|
||||||
|
// Test simple fields
|
||||||
|
"public_ip": "203.0.113.1",
|
||||||
|
"private_ip": "192.168.1.1",
|
||||||
|
"protected_ip": "100.64.0.1",
|
||||||
|
"well_known_ip": "8.8.8.8",
|
||||||
|
"ipv6_addr": "2001:db8::1",
|
||||||
|
"private_ipv6": "fd00::1",
|
||||||
|
"domain": "test.example.com",
|
||||||
|
"uri": "stun:stun.example.com:3478",
|
||||||
|
"uri_with_ip": "turn:203.0.113.1:3478",
|
||||||
|
"netbird_domain": "device.netbird.cloud",
|
||||||
|
|
||||||
|
// Test CIDR ranges
|
||||||
|
"public_cidr": "203.0.113.0/24",
|
||||||
|
"private_cidr": "192.168.0.0/16",
|
||||||
|
"protected_cidr": "100.64.0.0/10",
|
||||||
|
"ipv6_cidr": "2001:db8::/32",
|
||||||
|
"private_ipv6_cidr": "fd00::/8",
|
||||||
|
|
||||||
|
// Test nested structures
|
||||||
|
"nested": map[string]any{
|
||||||
|
"ip": "203.0.113.2",
|
||||||
|
"domain": "nested.example.com",
|
||||||
|
"more_nest": map[string]any{
|
||||||
|
"ip": "203.0.113.3",
|
||||||
|
"domain": "deep.example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test arrays
|
||||||
|
"string_array": []any{
|
||||||
|
"203.0.113.4",
|
||||||
|
"test1.example.com",
|
||||||
|
"test2.example.com",
|
||||||
|
},
|
||||||
|
"object_array": []any{
|
||||||
|
map[string]any{
|
||||||
|
"ip": "203.0.113.5",
|
||||||
|
"domain": "array1.example.com",
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"ip": "203.0.113.6",
|
||||||
|
"domain": "array2.example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test multiple occurrences of same value
|
||||||
|
"duplicate_ip": "203.0.113.1", // Same as public_ip
|
||||||
|
"duplicate_domain": "test.example.com", // Same as domain
|
||||||
|
|
||||||
|
// Test URIs with various schemes
|
||||||
|
"stun_uri": "stun:stun.example.com:3478",
|
||||||
|
"turns_uri": "turns:turns.example.com:5349",
|
||||||
|
"http_uri": "http://web.example.com:80",
|
||||||
|
"https_uri": "https://secure.example.com:443",
|
||||||
|
|
||||||
|
// Test strings that might look like IPs but aren't
|
||||||
|
"not_ip": "300.300.300.300",
|
||||||
|
"partial_ip": "192.168",
|
||||||
|
"ip_like_string": "1234.5678",
|
||||||
|
|
||||||
|
// Test mixed content strings
|
||||||
|
"mixed_content": "Server at 203.0.113.1 (test.example.com) on port 80",
|
||||||
|
|
||||||
|
// Test empty and special values
|
||||||
|
"empty_string": "",
|
||||||
|
"null_value": nil,
|
||||||
|
"numeric_value": 42,
|
||||||
|
"boolean_value": true,
|
||||||
|
}),
|
||||||
|
"route_state": mustMarshal(map[string]any{
|
||||||
|
"routes": []any{
|
||||||
|
map[string]any{
|
||||||
|
"network": "203.0.113.0/24",
|
||||||
|
"gateway": "203.0.113.1",
|
||||||
|
"domains": []any{
|
||||||
|
"route1.example.com",
|
||||||
|
"route2.example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"network": "2001:db8::/32",
|
||||||
|
"gateway": "2001:db8::1",
|
||||||
|
"domains": []any{
|
||||||
|
"route3.example.com",
|
||||||
|
"route4.example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Test map with IP/CIDR keys
|
||||||
|
"refCountMap": map[string]any{
|
||||||
|
"203.0.113.1/32": map[string]any{
|
||||||
|
"Count": 1,
|
||||||
|
"Out": map[string]any{
|
||||||
|
"IP": "192.168.0.1",
|
||||||
|
"Intf": map[string]any{
|
||||||
|
"Name": "eth0",
|
||||||
|
"Index": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2001:db8::1/128": map[string]any{
|
||||||
|
"Count": 1,
|
||||||
|
"Out": map[string]any{
|
||||||
|
"IP": "fe80::1",
|
||||||
|
"Intf": map[string]any{
|
||||||
|
"Name": "eth0",
|
||||||
|
"Index": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"10.0.0.1/32": map[string]any{ // private IP should remain unchanged
|
||||||
|
"Count": 1,
|
||||||
|
"Out": map[string]any{
|
||||||
|
"IP": "192.168.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||||
|
|
||||||
|
// Pre-seed the domains we need to verify in the test assertions
|
||||||
|
anonymizer.AnonymizeDomain("test.example.com")
|
||||||
|
anonymizer.AnonymizeDomain("nested.example.com")
|
||||||
|
anonymizer.AnonymizeDomain("deep.example.com")
|
||||||
|
anonymizer.AnonymizeDomain("array1.example.com")
|
||||||
|
|
||||||
|
err := anonymizeStateFile(&testState, anonymizer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Helper function to unmarshal and get nested values
|
||||||
|
var state map[string]any
|
||||||
|
err = json.Unmarshal(testState["test_state"], &state)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test null state remains unchanged
|
||||||
|
require.Equal(t, "null", string(testState["null_state"]))
|
||||||
|
|
||||||
|
// Basic assertions
|
||||||
|
assert.NotEqual(t, "203.0.113.1", state["public_ip"])
|
||||||
|
assert.Equal(t, "192.168.1.1", state["private_ip"]) // Private IP unchanged
|
||||||
|
assert.Equal(t, "100.64.0.1", state["protected_ip"]) // Protected IP unchanged
|
||||||
|
assert.Equal(t, "8.8.8.8", state["well_known_ip"]) // Well-known IP unchanged
|
||||||
|
assert.NotEqual(t, "2001:db8::1", state["ipv6_addr"])
|
||||||
|
assert.Equal(t, "fd00::1", state["private_ipv6"]) // Private IPv6 unchanged
|
||||||
|
assert.NotEqual(t, "test.example.com", state["domain"])
|
||||||
|
assert.True(t, strings.HasSuffix(state["domain"].(string), ".domain"))
|
||||||
|
assert.Equal(t, "device.netbird.cloud", state["netbird_domain"]) // Netbird domain unchanged
|
||||||
|
|
||||||
|
// CIDR ranges
|
||||||
|
assert.NotEqual(t, "203.0.113.0/24", state["public_cidr"])
|
||||||
|
assert.Contains(t, state["public_cidr"], "/24") // Prefix preserved
|
||||||
|
assert.Equal(t, "192.168.0.0/16", state["private_cidr"]) // Private CIDR unchanged
|
||||||
|
assert.Equal(t, "100.64.0.0/10", state["protected_cidr"]) // Protected CIDR unchanged
|
||||||
|
assert.NotEqual(t, "2001:db8::/32", state["ipv6_cidr"])
|
||||||
|
assert.Contains(t, state["ipv6_cidr"], "/32") // IPv6 prefix preserved
|
||||||
|
|
||||||
|
// Nested structures
|
||||||
|
nested := state["nested"].(map[string]any)
|
||||||
|
assert.NotEqual(t, "203.0.113.2", nested["ip"])
|
||||||
|
assert.NotEqual(t, "nested.example.com", nested["domain"])
|
||||||
|
moreNest := nested["more_nest"].(map[string]any)
|
||||||
|
assert.NotEqual(t, "203.0.113.3", moreNest["ip"])
|
||||||
|
assert.NotEqual(t, "deep.example.com", moreNest["domain"])
|
||||||
|
|
||||||
|
// Arrays
|
||||||
|
strArray := state["string_array"].([]any)
|
||||||
|
assert.NotEqual(t, "203.0.113.4", strArray[0])
|
||||||
|
assert.NotEqual(t, "test1.example.com", strArray[1])
|
||||||
|
assert.True(t, strings.HasSuffix(strArray[1].(string), ".domain"))
|
||||||
|
|
||||||
|
objArray := state["object_array"].([]any)
|
||||||
|
firstObj := objArray[0].(map[string]any)
|
||||||
|
assert.NotEqual(t, "203.0.113.5", firstObj["ip"])
|
||||||
|
assert.NotEqual(t, "array1.example.com", firstObj["domain"])
|
||||||
|
|
||||||
|
// Duplicate values should be anonymized consistently
|
||||||
|
assert.Equal(t, state["public_ip"], state["duplicate_ip"])
|
||||||
|
assert.Equal(t, state["domain"], state["duplicate_domain"])
|
||||||
|
|
||||||
|
// URIs
|
||||||
|
assert.NotContains(t, state["stun_uri"], "stun.example.com")
|
||||||
|
assert.NotContains(t, state["turns_uri"], "turns.example.com")
|
||||||
|
assert.NotContains(t, state["http_uri"], "web.example.com")
|
||||||
|
assert.NotContains(t, state["https_uri"], "secure.example.com")
|
||||||
|
|
||||||
|
// Non-IP strings should remain unchanged
|
||||||
|
assert.Equal(t, "300.300.300.300", state["not_ip"])
|
||||||
|
assert.Equal(t, "192.168", state["partial_ip"])
|
||||||
|
assert.Equal(t, "1234.5678", state["ip_like_string"])
|
||||||
|
|
||||||
|
// Mixed content should have IPs and domains replaced
|
||||||
|
mixedContent := state["mixed_content"].(string)
|
||||||
|
assert.NotContains(t, mixedContent, "203.0.113.1")
|
||||||
|
assert.NotContains(t, mixedContent, "test.example.com")
|
||||||
|
assert.Contains(t, mixedContent, "Server at ")
|
||||||
|
assert.Contains(t, mixedContent, " on port 80")
|
||||||
|
|
||||||
|
// Special values should remain unchanged
|
||||||
|
assert.Equal(t, "", state["empty_string"])
|
||||||
|
assert.Nil(t, state["null_value"])
|
||||||
|
assert.Equal(t, float64(42), state["numeric_value"])
|
||||||
|
assert.Equal(t, true, state["boolean_value"])
|
||||||
|
|
||||||
|
// Check route state
|
||||||
|
var routeState map[string]any
|
||||||
|
err = json.Unmarshal(testState["route_state"], &routeState)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
routes := routeState["routes"].([]any)
|
||||||
|
route1 := routes[0].(map[string]any)
|
||||||
|
assert.NotEqual(t, "203.0.113.0/24", route1["network"])
|
||||||
|
assert.Contains(t, route1["network"], "/24")
|
||||||
|
assert.NotEqual(t, "203.0.113.1", route1["gateway"])
|
||||||
|
domains := route1["domains"].([]any)
|
||||||
|
assert.True(t, strings.HasSuffix(domains[0].(string), ".domain"))
|
||||||
|
assert.True(t, strings.HasSuffix(domains[1].(string), ".domain"))
|
||||||
|
|
||||||
|
// Check map keys are anonymized
|
||||||
|
refCountMap := routeState["refCountMap"].(map[string]any)
|
||||||
|
hasPublicIPKey := false
|
||||||
|
hasIPv6Key := false
|
||||||
|
hasPrivateIPKey := false
|
||||||
|
for key := range refCountMap {
|
||||||
|
if strings.Contains(key, "203.0.113.1") {
|
||||||
|
hasPublicIPKey = true
|
||||||
|
}
|
||||||
|
if strings.Contains(key, "2001:db8::1") {
|
||||||
|
hasIPv6Key = true
|
||||||
|
}
|
||||||
|
if key == "10.0.0.1/32" {
|
||||||
|
hasPrivateIPKey = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.False(t, hasPublicIPKey, "public IP in key should be anonymized")
|
||||||
|
assert.False(t, hasIPv6Key, "IPv6 in key should be anonymized")
|
||||||
|
assert.True(t, hasPrivateIPKey, "private IP in key should remain unchanged")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustMarshal(v any) json.RawMessage {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeNetworkMap(t *testing.T) {
|
||||||
|
networkMap := &mgmProto.NetworkMap{
|
||||||
|
PeerConfig: &mgmProto.PeerConfig{
|
||||||
|
Address: "203.0.113.5",
|
||||||
|
Dns: "1.2.3.4",
|
||||||
|
Fqdn: "peer1.corp.example.com",
|
||||||
|
SshConfig: &mgmProto.SSHConfig{
|
||||||
|
SshPubKey: []byte("ssh-rsa AAAAB3NzaC1..."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RemotePeers: []*mgmProto.RemotePeerConfig{
|
||||||
|
{
|
||||||
|
AllowedIps: []string{
|
||||||
|
"203.0.113.1/32",
|
||||||
|
"2001:db8:1234::1/128",
|
||||||
|
"192.168.1.1/32",
|
||||||
|
"100.64.0.1/32",
|
||||||
|
"10.0.0.1/32",
|
||||||
|
},
|
||||||
|
Fqdn: "peer2.corp.example.com",
|
||||||
|
SshConfig: &mgmProto.SSHConfig{
|
||||||
|
SshPubKey: []byte("ssh-rsa AAAAB3NzaC2..."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Routes: []*mgmProto.Route{
|
||||||
|
{
|
||||||
|
Network: "197.51.100.0/24",
|
||||||
|
Domains: []string{"prod.example.com", "staging.example.com"},
|
||||||
|
NetID: "net-123abc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DNSConfig: &mgmProto.DNSConfig{
|
||||||
|
NameServerGroups: []*mgmProto.NameServerGroup{
|
||||||
|
{
|
||||||
|
NameServers: []*mgmProto.NameServer{
|
||||||
|
{IP: "8.8.8.8"},
|
||||||
|
{IP: "1.1.1.1"},
|
||||||
|
{IP: "203.0.113.53"},
|
||||||
|
},
|
||||||
|
Domains: []string{"example.com", "internal.example.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CustomZones: []*mgmProto.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "custom.example.com",
|
||||||
|
Records: []*mgmProto.SimpleRecord{
|
||||||
|
{
|
||||||
|
Name: "www.custom.example.com",
|
||||||
|
Type: 1,
|
||||||
|
RData: "203.0.113.10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "internal.custom.example.com",
|
||||||
|
Type: 1,
|
||||||
|
RData: "192.168.1.10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create anonymizer with test addresses
|
||||||
|
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||||
|
|
||||||
|
// Anonymize the network map
|
||||||
|
err := anonymizeNetworkMap(networkMap, anonymizer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test PeerConfig anonymization
|
||||||
|
peerCfg := networkMap.PeerConfig
|
||||||
|
require.NotEqual(t, "203.0.113.5", peerCfg.Address)
|
||||||
|
|
||||||
|
// Verify DNS and FQDN are properly anonymized
|
||||||
|
require.NotEqual(t, "1.2.3.4", peerCfg.Dns)
|
||||||
|
require.NotEqual(t, "peer1.corp.example.com", peerCfg.Fqdn)
|
||||||
|
require.True(t, strings.HasSuffix(peerCfg.Fqdn, ".domain"))
|
||||||
|
|
||||||
|
// Verify SSH key is replaced
|
||||||
|
require.Equal(t, []byte("ssh-placeholder-key"), peerCfg.SshConfig.SshPubKey)
|
||||||
|
|
||||||
|
// Test RemotePeers anonymization
|
||||||
|
remotePeer := networkMap.RemotePeers[0]
|
||||||
|
|
||||||
|
// Verify FQDN is anonymized
|
||||||
|
require.NotEqual(t, "peer2.corp.example.com", remotePeer.Fqdn)
|
||||||
|
require.True(t, strings.HasSuffix(remotePeer.Fqdn, ".domain"))
|
||||||
|
|
||||||
|
// Check that public IPs are anonymized but private IPs are preserved
|
||||||
|
for _, allowedIP := range remotePeer.AllowedIps {
|
||||||
|
ip, _, err := net.ParseCIDR(allowedIP)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if ip.IsPrivate() || isInCGNATRange(ip) {
|
||||||
|
require.Contains(t, []string{
|
||||||
|
"192.168.1.1/32",
|
||||||
|
"100.64.0.1/32",
|
||||||
|
"10.0.0.1/32",
|
||||||
|
}, allowedIP)
|
||||||
|
} else {
|
||||||
|
require.NotContains(t, []string{
|
||||||
|
"203.0.113.1/32",
|
||||||
|
"2001:db8:1234::1/128",
|
||||||
|
}, allowedIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Routes anonymization
|
||||||
|
route := networkMap.Routes[0]
|
||||||
|
require.NotEqual(t, "197.51.100.0/24", route.Network)
|
||||||
|
for _, domain := range route.Domains {
|
||||||
|
require.True(t, strings.HasSuffix(domain, ".domain"))
|
||||||
|
require.NotContains(t, domain, "example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test DNS config anonymization
|
||||||
|
dnsConfig := networkMap.DNSConfig
|
||||||
|
nameServerGroup := dnsConfig.NameServerGroups[0]
|
||||||
|
|
||||||
|
// Verify well-known DNS servers are preserved
|
||||||
|
require.Equal(t, "8.8.8.8", nameServerGroup.NameServers[0].IP)
|
||||||
|
require.Equal(t, "1.1.1.1", nameServerGroup.NameServers[1].IP)
|
||||||
|
|
||||||
|
// Verify public DNS server is anonymized
|
||||||
|
require.NotEqual(t, "203.0.113.53", nameServerGroup.NameServers[2].IP)
|
||||||
|
|
||||||
|
// Verify domains are anonymized
|
||||||
|
for _, domain := range nameServerGroup.Domains {
|
||||||
|
require.True(t, strings.HasSuffix(domain, ".domain"))
|
||||||
|
require.NotContains(t, domain, "example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test CustomZones anonymization
|
||||||
|
customZone := dnsConfig.CustomZones[0]
|
||||||
|
require.True(t, strings.HasSuffix(customZone.Domain, ".domain"))
|
||||||
|
require.NotContains(t, customZone.Domain, "example.com")
|
||||||
|
|
||||||
|
// Verify records are properly anonymized
|
||||||
|
for _, record := range customZone.Records {
|
||||||
|
require.True(t, strings.HasSuffix(record.Name, ".domain"))
|
||||||
|
require.NotContains(t, record.Name, "example.com")
|
||||||
|
|
||||||
|
ip := net.ParseIP(record.RData)
|
||||||
|
if ip != nil {
|
||||||
|
if !ip.IsPrivate() {
|
||||||
|
require.NotEqual(t, "203.0.113.10", record.RData)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, "192.168.1.10", record.RData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if IP is in CGNAT range
|
||||||
|
func isInCGNATRange(ip net.IP) bool {
|
||||||
|
cgnat := net.IPNet{
|
||||||
|
IP: net.ParseIP("100.64.0.0"),
|
||||||
|
Mask: net.CIDRMask(10, 32),
|
||||||
|
}
|
||||||
|
return cgnat.Contains(ip)
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
@@ -20,8 +21,8 @@ type selectRoute struct {
|
|||||||
Selected bool
|
Selected bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListRoutes returns a list of all available routes.
|
// ListNetworks returns a list of all available networks.
|
||||||
func (s *Server) ListRoutes(context.Context, *proto.ListRoutesRequest) (*proto.ListRoutesResponse, error) {
|
func (s *Server) ListNetworks(context.Context, *proto.ListNetworksRequest) (*proto.ListNetworksResponse, error) {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ func (s *Server) ListRoutes(context.Context, *proto.ListRoutesRequest) (*proto.L
|
|||||||
return nil, fmt.Errorf("not connected")
|
return nil, fmt.Errorf("not connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
routesMap := engine.GetClientRoutesWithNetID()
|
routesMap := engine.GetRouteManager().GetClientRoutesWithNetID()
|
||||||
routeSelector := engine.GetRouteManager().GetRouteSelector()
|
routeSelector := engine.GetRouteManager().GetRouteSelector()
|
||||||
|
|
||||||
var routes []*selectRoute
|
var routes []*selectRoute
|
||||||
@@ -67,37 +68,47 @@ func (s *Server) ListRoutes(context.Context, *proto.ListRoutesRequest) (*proto.L
|
|||||||
})
|
})
|
||||||
|
|
||||||
resolvedDomains := s.statusRecorder.GetResolvedDomainsStates()
|
resolvedDomains := s.statusRecorder.GetResolvedDomainsStates()
|
||||||
var pbRoutes []*proto.Route
|
var pbRoutes []*proto.Network
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
pbRoute := &proto.Route{
|
pbRoute := &proto.Network{
|
||||||
ID: string(route.NetID),
|
ID: string(route.NetID),
|
||||||
Network: route.Network.String(),
|
Range: route.Network.String(),
|
||||||
Domains: route.Domains.ToSafeStringList(),
|
Domains: route.Domains.ToSafeStringList(),
|
||||||
ResolvedIPs: map[string]*proto.IPList{},
|
ResolvedIPs: map[string]*proto.IPList{},
|
||||||
Selected: route.Selected,
|
Selected: route.Selected,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, domain := range route.Domains {
|
// Group resolved IPs by their parent domain
|
||||||
if prefixes, exists := resolvedDomains[domain]; exists {
|
domainMap := map[domain.Domain][]string{}
|
||||||
var ipStrings []string
|
|
||||||
for _, prefix := range prefixes {
|
for resolvedDomain, info := range resolvedDomains {
|
||||||
ipStrings = append(ipStrings, prefix.Addr().String())
|
// Check if this resolved domain's parent is in our route's domains
|
||||||
}
|
if slices.Contains(route.Domains, info.ParentDomain) {
|
||||||
pbRoute.ResolvedIPs[string(domain)] = &proto.IPList{
|
ips := make([]string, 0, len(info.Prefixes))
|
||||||
Ips: ipStrings,
|
for _, prefix := range info.Prefixes {
|
||||||
|
ips = append(ips, prefix.Addr().String())
|
||||||
}
|
}
|
||||||
|
domainMap[resolvedDomain] = ips
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert to proto format
|
||||||
|
for domain, ips := range domainMap {
|
||||||
|
pbRoute.ResolvedIPs[string(domain)] = &proto.IPList{
|
||||||
|
Ips: ips,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pbRoutes = append(pbRoutes, pbRoute)
|
pbRoutes = append(pbRoutes, pbRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &proto.ListRoutesResponse{
|
return &proto.ListNetworksResponse{
|
||||||
Routes: pbRoutes,
|
Routes: pbRoutes,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SelectRoutes selects specific routes based on the client request.
|
// SelectNetworks selects specific networks based on the client request.
|
||||||
func (s *Server) SelectRoutes(_ context.Context, req *proto.SelectRoutesRequest) (*proto.SelectRoutesResponse, error) {
|
func (s *Server) SelectNetworks(_ context.Context, req *proto.SelectNetworksRequest) (*proto.SelectNetworksResponse, error) {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
@@ -115,18 +126,19 @@ func (s *Server) SelectRoutes(_ context.Context, req *proto.SelectRoutesRequest)
|
|||||||
if req.GetAll() {
|
if req.GetAll() {
|
||||||
routeSelector.SelectAllRoutes()
|
routeSelector.SelectAllRoutes()
|
||||||
} else {
|
} else {
|
||||||
routes := toNetIDs(req.GetRouteIDs())
|
routes := toNetIDs(req.GetNetworkIDs())
|
||||||
if err := routeSelector.SelectRoutes(routes, req.GetAppend(), maps.Keys(engine.GetClientRoutesWithNetID())); err != nil {
|
netIdRoutes := maps.Keys(routeManager.GetClientRoutesWithNetID())
|
||||||
|
if err := routeSelector.SelectRoutes(routes, req.GetAppend(), netIdRoutes); err != nil {
|
||||||
return nil, fmt.Errorf("select routes: %w", err)
|
return nil, fmt.Errorf("select routes: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
routeManager.TriggerSelection(engine.GetClientRoutes())
|
routeManager.TriggerSelection(routeManager.GetClientRoutes())
|
||||||
|
|
||||||
return &proto.SelectRoutesResponse{}, nil
|
return &proto.SelectNetworksResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeselectRoutes deselects specific routes based on the client request.
|
// DeselectNetworks deselects specific networks based on the client request.
|
||||||
func (s *Server) DeselectRoutes(_ context.Context, req *proto.SelectRoutesRequest) (*proto.SelectRoutesResponse, error) {
|
func (s *Server) DeselectNetworks(_ context.Context, req *proto.SelectNetworksRequest) (*proto.SelectNetworksResponse, error) {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
@@ -144,14 +156,15 @@ func (s *Server) DeselectRoutes(_ context.Context, req *proto.SelectRoutesReques
|
|||||||
if req.GetAll() {
|
if req.GetAll() {
|
||||||
routeSelector.DeselectAllRoutes()
|
routeSelector.DeselectAllRoutes()
|
||||||
} else {
|
} else {
|
||||||
routes := toNetIDs(req.GetRouteIDs())
|
routes := toNetIDs(req.GetNetworkIDs())
|
||||||
if err := routeSelector.DeselectRoutes(routes, maps.Keys(engine.GetClientRoutesWithNetID())); err != nil {
|
netIdRoutes := maps.Keys(routeManager.GetClientRoutesWithNetID())
|
||||||
|
if err := routeSelector.DeselectRoutes(routes, netIdRoutes); err != nil {
|
||||||
return nil, fmt.Errorf("deselect routes: %w", err)
|
return nil, fmt.Errorf("deselect routes: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
routeManager.TriggerSelection(engine.GetClientRoutes())
|
routeManager.TriggerSelection(routeManager.GetClientRoutes())
|
||||||
|
|
||||||
return &proto.SelectRoutesResponse{}, nil
|
return &proto.SelectNetworksResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func toNetIDs(routes []string) []route.NetID {
|
func toNetIDs(routes []string) []route.NetID {
|
||||||
7
client/server/panic_generic.go
Normal file
7
client/server/panic_generic.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
func handlePanicLog() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
83
client/server/panic_windows.go
Normal file
83
client/server/panic_windows.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
windowsPanicLogEnvVar = "NB_WINDOWS_PANIC_LOG"
|
||||||
|
// STD_ERROR_HANDLE ((DWORD)-12) = 4294967284
|
||||||
|
stdErrorHandle = ^uintptr(11)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/windows/console/setstdhandle
|
||||||
|
setStdHandleFn = kernel32.NewProc("SetStdHandle")
|
||||||
|
)
|
||||||
|
|
||||||
|
func handlePanicLog() error {
|
||||||
|
logPath := os.Getenv(windowsPanicLogEnvVar)
|
||||||
|
if logPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the directory exists
|
||||||
|
logDir := filepath.Dir(logPath)
|
||||||
|
if err := os.MkdirAll(logDir, 0750); err != nil {
|
||||||
|
return fmt.Errorf("create panic log directory: %w", err)
|
||||||
|
}
|
||||||
|
if err := util.EnforcePermission(logPath); err != nil {
|
||||||
|
return fmt.Errorf("enforce permission on panic log file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open log file with append mode
|
||||||
|
f, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open panic log file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect stderr to the file
|
||||||
|
if err = redirectStderr(f); err != nil {
|
||||||
|
if closeErr := f.Close(); closeErr != nil {
|
||||||
|
log.Warnf("failed to close file after redirect error: %v", closeErr)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("redirect stderr: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("successfully configured panic logging to: %s", logPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirectStderr redirects stderr to the provided file
|
||||||
|
func redirectStderr(f *os.File) error {
|
||||||
|
// Get the current process's stderr handle
|
||||||
|
if err := setStdHandle(f); err != nil {
|
||||||
|
return fmt.Errorf("failed to set stderr handle: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also set os.Stderr for Go's standard library
|
||||||
|
os.Stderr = f
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setStdHandle(f *os.File) error {
|
||||||
|
handle := f.Fd()
|
||||||
|
r0, _, e1 := setStdHandleFn.Call(stdErrorHandle, handle)
|
||||||
|
if r0 == 0 {
|
||||||
|
if e1 != nil {
|
||||||
|
return e1
|
||||||
|
}
|
||||||
|
return syscall.EINVAL
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -68,6 +68,8 @@ type Server struct {
|
|||||||
relayProbe *internal.Probe
|
relayProbe *internal.Probe
|
||||||
wgProbe *internal.Probe
|
wgProbe *internal.Probe
|
||||||
lastProbe time.Time
|
lastProbe time.Time
|
||||||
|
|
||||||
|
persistNetworkMap bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type oauthAuthFlow struct {
|
type oauthAuthFlow struct {
|
||||||
@@ -97,6 +99,10 @@ func (s *Server) Start() error {
|
|||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
state := internal.CtxGetState(s.rootCtx)
|
state := internal.CtxGetState(s.rootCtx)
|
||||||
|
|
||||||
|
if err := handlePanicLog(); err != nil {
|
||||||
|
log.Warnf("failed to redirect stderr: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := restoreResidualState(s.rootCtx); err != nil {
|
if err := restoreResidualState(s.rootCtx); err != nil {
|
||||||
log.Warnf(errRestoreResidualState, err)
|
log.Warnf(errRestoreResidualState, err)
|
||||||
}
|
}
|
||||||
@@ -192,6 +198,7 @@ func (s *Server) connectWithRetryRuns(ctx context.Context, config *internal.Conf
|
|||||||
runOperation := func() error {
|
runOperation := func() error {
|
||||||
log.Tracef("running client connection")
|
log.Tracef("running client connection")
|
||||||
s.connectClient = internal.NewConnectClient(ctx, config, statusRecorder)
|
s.connectClient = internal.NewConnectClient(ctx, config, statusRecorder)
|
||||||
|
s.connectClient.SetNetworkMapPersistence(s.persistNetworkMap)
|
||||||
|
|
||||||
probes := internal.ProbeHolder{
|
probes := internal.ProbeHolder{
|
||||||
MgmProbe: s.mgmProbe,
|
MgmProbe: s.mgmProbe,
|
||||||
@@ -622,6 +629,8 @@ func (s *Server) Down(ctx context.Context, _ *proto.DownRequest) (*proto.DownRes
|
|||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.oauthAuthFlow = oauthAuthFlow{}
|
||||||
|
|
||||||
if s.actCancel == nil {
|
if s.actCancel == nil {
|
||||||
return nil, fmt.Errorf("service is not up")
|
return nil, fmt.Errorf("service is not up")
|
||||||
}
|
}
|
||||||
@@ -763,7 +772,7 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus {
|
|||||||
pbFullStatus.LocalPeerState.Fqdn = fullStatus.LocalPeerState.FQDN
|
pbFullStatus.LocalPeerState.Fqdn = fullStatus.LocalPeerState.FQDN
|
||||||
pbFullStatus.LocalPeerState.RosenpassPermissive = fullStatus.RosenpassState.Permissive
|
pbFullStatus.LocalPeerState.RosenpassPermissive = fullStatus.RosenpassState.Permissive
|
||||||
pbFullStatus.LocalPeerState.RosenpassEnabled = fullStatus.RosenpassState.Enabled
|
pbFullStatus.LocalPeerState.RosenpassEnabled = fullStatus.RosenpassState.Enabled
|
||||||
pbFullStatus.LocalPeerState.Routes = maps.Keys(fullStatus.LocalPeerState.Routes)
|
pbFullStatus.LocalPeerState.Networks = maps.Keys(fullStatus.LocalPeerState.Routes)
|
||||||
|
|
||||||
for _, peerState := range fullStatus.Peers {
|
for _, peerState := range fullStatus.Peers {
|
||||||
pbPeerState := &proto.PeerState{
|
pbPeerState := &proto.PeerState{
|
||||||
@@ -782,7 +791,7 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus {
|
|||||||
BytesRx: peerState.BytesRx,
|
BytesRx: peerState.BytesRx,
|
||||||
BytesTx: peerState.BytesTx,
|
BytesTx: peerState.BytesTx,
|
||||||
RosenpassEnabled: peerState.RosenpassEnabled,
|
RosenpassEnabled: peerState.RosenpassEnabled,
|
||||||
Routes: maps.Keys(peerState.GetRoutes()),
|
Networks: maps.Keys(peerState.GetRoutes()),
|
||||||
Latency: durationpb.New(peerState.Latency),
|
Latency: durationpb.New(peerState.Latency),
|
||||||
}
|
}
|
||||||
pbFullStatus.Peers = append(pbFullStatus.Peers, pbPeerState)
|
pbFullStatus.Peers = append(pbFullStatus.Peers, pbPeerState)
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import (
|
|||||||
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
||||||
"github.com/netbirdio/netbird/management/server"
|
"github.com/netbirdio/netbird/management/server"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
"github.com/netbirdio/netbird/signal/proto"
|
"github.com/netbirdio/netbird/signal/proto"
|
||||||
signalServer "github.com/netbirdio/netbird/signal/server"
|
signalServer "github.com/netbirdio/netbird/signal/server"
|
||||||
@@ -110,7 +112,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
|
|||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
||||||
store, cleanUp, err := server.NewTestStoreFromSQL(context.Background(), "", config.Datadir)
|
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "", config.Datadir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
@@ -132,7 +134,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
|
|||||||
}
|
}
|
||||||
|
|
||||||
secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
|
secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
|
||||||
mgmtServer, err := server.NewServer(context.Background(), config, accountManager, peersUpdateManager, secretsManager, nil, nil)
|
mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,112 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// restoreResidualConfig checks if the client was not shut down in a clean way and restores residual state if required.
|
// ListStates returns a list of all saved states
|
||||||
|
func (s *Server) ListStates(_ context.Context, _ *proto.ListStatesRequest) (*proto.ListStatesResponse, error) {
|
||||||
|
mgr := statemanager.New(statemanager.GetDefaultStatePath())
|
||||||
|
|
||||||
|
stateNames, err := mgr.GetSavedStateNames()
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to get saved state names: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
states := make([]*proto.State, 0, len(stateNames))
|
||||||
|
for _, name := range stateNames {
|
||||||
|
states = append(states, &proto.State{
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &proto.ListStatesResponse{
|
||||||
|
States: states,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanState handles cleaning of states (performing cleanup operations)
|
||||||
|
func (s *Server) CleanState(ctx context.Context, req *proto.CleanStateRequest) (*proto.CleanStateResponse, error) {
|
||||||
|
if s.connectClient.Status() == internal.StatusConnected || s.connectClient.Status() == internal.StatusConnecting {
|
||||||
|
return nil, status.Errorf(codes.FailedPrecondition, "cannot clean state while connecting or connected, run 'netbird down' first.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.All {
|
||||||
|
// Reuse existing cleanup logic for all states
|
||||||
|
if err := restoreResidualState(ctx); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to clean all states: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of cleaned states
|
||||||
|
mgr := statemanager.New(statemanager.GetDefaultStatePath())
|
||||||
|
stateNames, err := mgr.GetSavedStateNames()
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to get state count: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &proto.CleanStateResponse{
|
||||||
|
CleanedStates: int32(len(stateNames)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle single state cleanup
|
||||||
|
mgr := statemanager.New(statemanager.GetDefaultStatePath())
|
||||||
|
registerStates(mgr)
|
||||||
|
|
||||||
|
if err := mgr.CleanupStateByName(req.StateName); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to clean state %s: %v", req.StateName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mgr.PersistState(ctx); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to persist state changes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &proto.CleanStateResponse{
|
||||||
|
CleanedStates: 1,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteState handles deletion of states without cleanup
|
||||||
|
func (s *Server) DeleteState(ctx context.Context, req *proto.DeleteStateRequest) (*proto.DeleteStateResponse, error) {
|
||||||
|
if s.connectClient.Status() == internal.StatusConnected || s.connectClient.Status() == internal.StatusConnecting {
|
||||||
|
return nil, status.Errorf(codes.FailedPrecondition, "cannot clean state while connecting or connected, run 'netbird down' first.")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr := statemanager.New(statemanager.GetDefaultStatePath())
|
||||||
|
|
||||||
|
var count int
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if req.All {
|
||||||
|
count, err = mgr.DeleteAllStates()
|
||||||
|
} else {
|
||||||
|
err = mgr.DeleteStateByName(req.StateName)
|
||||||
|
if err == nil {
|
||||||
|
count = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to delete state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist the changes
|
||||||
|
if err := mgr.PersistState(ctx); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to persist state changes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &proto.DeleteStateResponse{
|
||||||
|
DeletedStates: int32(count),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// restoreResidualState checks if the client was not shut down in a clean way and restores residual if required.
|
||||||
// Otherwise, we might not be able to connect to the management server to retrieve new config.
|
// Otherwise, we might not be able to connect to the management server to retrieve new config.
|
||||||
func restoreResidualState(ctx context.Context) error {
|
func restoreResidualState(ctx context.Context) error {
|
||||||
path := statemanager.GetDefaultStatePath()
|
path := statemanager.GetDefaultStatePath()
|
||||||
@@ -24,6 +124,7 @@ func restoreResidualState(ctx context.Context) error {
|
|||||||
registerStates(mgr)
|
registerStates(mgr)
|
||||||
|
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
|
|
||||||
if err := mgr.PerformCleanup(); err != nil {
|
if err := mgr.PerformCleanup(); err != nil {
|
||||||
merr = multierror.Append(merr, fmt.Errorf("perform cleanup: %w", err))
|
merr = multierror.Append(merr, fmt.Errorf("perform cleanup: %w", err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,14 @@ type Info struct {
|
|||||||
Files []File // for posture checks
|
Files []File // for posture checks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StaticInfo is an object that contains machine information that does not change
|
||||||
|
type StaticInfo struct {
|
||||||
|
SystemSerialNumber string
|
||||||
|
SystemProductName string
|
||||||
|
SystemManufacturer string
|
||||||
|
Environment Environment
|
||||||
|
}
|
||||||
|
|
||||||
// extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context
|
// extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context
|
||||||
func extractUserAgent(ctx context.Context) string {
|
func extractUserAgent(ctx context.Context) string {
|
||||||
md, hasMeta := metadata.FromOutgoingContext(ctx)
|
md, hasMeta := metadata.FromOutgoingContext(ctx)
|
||||||
|
|||||||
@@ -10,13 +10,12 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/system/detect_cloud"
|
|
||||||
"github.com/netbirdio/netbird/client/system/detect_platform"
|
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,11 +40,10 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
log.Warnf("failed to discover network addresses: %s", err)
|
log.Warnf("failed to discover network addresses: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
serialNum, prodName, manufacturer := sysInfo()
|
start := time.Now()
|
||||||
|
si := updateStaticInfo()
|
||||||
env := Environment{
|
if time.Since(start) > 1*time.Second {
|
||||||
Cloud: detect_cloud.Detect(ctx),
|
log.Warnf("updateStaticInfo took %s", time.Since(start))
|
||||||
Platform: detect_platform.Detect(ctx),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gio := &Info{
|
gio := &Info{
|
||||||
@@ -57,10 +55,10 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
CPUs: runtime.NumCPU(),
|
CPUs: runtime.NumCPU(),
|
||||||
KernelVersion: release,
|
KernelVersion: release,
|
||||||
NetworkAddresses: addrs,
|
NetworkAddresses: addrs,
|
||||||
SystemSerialNumber: serialNum,
|
SystemSerialNumber: si.SystemSerialNumber,
|
||||||
SystemProductName: prodName,
|
SystemProductName: si.SystemProductName,
|
||||||
SystemManufacturer: manufacturer,
|
SystemManufacturer: si.SystemManufacturer,
|
||||||
Environment: env,
|
Environment: si.Environment,
|
||||||
}
|
}
|
||||||
|
|
||||||
systemHostname, _ := os.Hostname()
|
systemHostname, _ := os.Hostname()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !android
|
//go:build !android
|
||||||
// +build !android
|
|
||||||
|
|
||||||
package system
|
package system
|
||||||
|
|
||||||
@@ -16,30 +15,13 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/zcalusic/sysinfo"
|
"github.com/zcalusic/sysinfo"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/system/detect_cloud"
|
|
||||||
"github.com/netbirdio/netbird/client/system/detect_platform"
|
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SysInfoGetter interface {
|
var (
|
||||||
GetSysInfo() SysInfo
|
// it is override in tests
|
||||||
}
|
getSystemInfo = defaultSysInfoImplementation
|
||||||
|
)
|
||||||
type SysInfoWrapper struct {
|
|
||||||
si sysinfo.SysInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SysInfoWrapper) GetSysInfo() SysInfo {
|
|
||||||
s.si.GetSysInfo()
|
|
||||||
return SysInfo{
|
|
||||||
ChassisSerial: s.si.Chassis.Serial,
|
|
||||||
ProductSerial: s.si.Product.Serial,
|
|
||||||
BoardSerial: s.si.Board.Serial,
|
|
||||||
ProductName: s.si.Product.Name,
|
|
||||||
BoardName: s.si.Board.Name,
|
|
||||||
ProductVendor: s.si.Product.Vendor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInfo retrieves and parses the system information
|
// GetInfo retrieves and parses the system information
|
||||||
func GetInfo(ctx context.Context) *Info {
|
func GetInfo(ctx context.Context) *Info {
|
||||||
@@ -65,12 +47,10 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
log.Warnf("failed to discover network addresses: %s", err)
|
log.Warnf("failed to discover network addresses: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
si := SysInfoWrapper{}
|
start := time.Now()
|
||||||
serialNum, prodName, manufacturer := sysInfo(si.GetSysInfo())
|
si := updateStaticInfo()
|
||||||
|
if time.Since(start) > 1*time.Second {
|
||||||
env := Environment{
|
log.Warnf("updateStaticInfo took %s", time.Since(start))
|
||||||
Cloud: detect_cloud.Detect(ctx),
|
|
||||||
Platform: detect_platform.Detect(ctx),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gio := &Info{
|
gio := &Info{
|
||||||
@@ -85,10 +65,10 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
UIVersion: extractUserAgent(ctx),
|
UIVersion: extractUserAgent(ctx),
|
||||||
KernelVersion: osInfo[1],
|
KernelVersion: osInfo[1],
|
||||||
NetworkAddresses: addrs,
|
NetworkAddresses: addrs,
|
||||||
SystemSerialNumber: serialNum,
|
SystemSerialNumber: si.SystemSerialNumber,
|
||||||
SystemProductName: prodName,
|
SystemProductName: si.SystemProductName,
|
||||||
SystemManufacturer: manufacturer,
|
SystemManufacturer: si.SystemManufacturer,
|
||||||
Environment: env,
|
Environment: si.Environment,
|
||||||
}
|
}
|
||||||
|
|
||||||
return gio
|
return gio
|
||||||
@@ -108,9 +88,9 @@ func _getInfo() string {
|
|||||||
return out.String()
|
return out.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func sysInfo(si SysInfo) (string, string, string) {
|
func sysInfo() (string, string, string) {
|
||||||
isascii := regexp.MustCompile("^[[:ascii:]]+$")
|
isascii := regexp.MustCompile("^[[:ascii:]]+$")
|
||||||
|
si := getSystemInfo()
|
||||||
serials := []string{si.ChassisSerial, si.ProductSerial}
|
serials := []string{si.ChassisSerial, si.ProductSerial}
|
||||||
serial := ""
|
serial := ""
|
||||||
|
|
||||||
@@ -141,3 +121,16 @@ func sysInfo(si SysInfo) (string, string, string) {
|
|||||||
}
|
}
|
||||||
return serial, name, manufacturer
|
return serial, name, manufacturer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultSysInfoImplementation() SysInfo {
|
||||||
|
si := sysinfo.SysInfo{}
|
||||||
|
si.GetSysInfo()
|
||||||
|
return SysInfo{
|
||||||
|
ChassisSerial: si.Chassis.Serial,
|
||||||
|
ProductSerial: si.Product.Serial,
|
||||||
|
BoardSerial: si.Board.Serial,
|
||||||
|
ProductName: si.Product.Name,
|
||||||
|
BoardName: si.Board.Name,
|
||||||
|
ProductVendor: si.Product.Vendor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/yusufpapurcu/wmi"
|
"github.com/yusufpapurcu/wmi"
|
||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/system/detect_cloud"
|
|
||||||
"github.com/netbirdio/netbird/client/system/detect_platform"
|
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,24 +41,10 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
log.Warnf("failed to discover network addresses: %s", err)
|
log.Warnf("failed to discover network addresses: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
serialNum, err := sysNumber()
|
start := time.Now()
|
||||||
if err != nil {
|
si := updateStaticInfo()
|
||||||
log.Warnf("failed to get system serial number: %s", err)
|
if time.Since(start) > 1*time.Second {
|
||||||
}
|
log.Warnf("updateStaticInfo took %s", time.Since(start))
|
||||||
|
|
||||||
prodName, err := sysProductName()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("failed to get system product name: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
manufacturer, err := sysManufacturer()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("failed to get system manufacturer: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
env := Environment{
|
|
||||||
Cloud: detect_cloud.Detect(ctx),
|
|
||||||
Platform: detect_platform.Detect(ctx),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gio := &Info{
|
gio := &Info{
|
||||||
@@ -71,10 +56,10 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
CPUs: runtime.NumCPU(),
|
CPUs: runtime.NumCPU(),
|
||||||
KernelVersion: buildVersion,
|
KernelVersion: buildVersion,
|
||||||
NetworkAddresses: addrs,
|
NetworkAddresses: addrs,
|
||||||
SystemSerialNumber: serialNum,
|
SystemSerialNumber: si.SystemSerialNumber,
|
||||||
SystemProductName: prodName,
|
SystemProductName: si.SystemProductName,
|
||||||
SystemManufacturer: manufacturer,
|
SystemManufacturer: si.SystemManufacturer,
|
||||||
Environment: env,
|
Environment: si.Environment,
|
||||||
}
|
}
|
||||||
|
|
||||||
systemHostname, _ := os.Hostname()
|
systemHostname, _ := os.Hostname()
|
||||||
@@ -85,6 +70,26 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
return gio
|
return gio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sysInfo() (serialNumber string, productName string, manufacturer string) {
|
||||||
|
var err error
|
||||||
|
serialNumber, err = sysNumber()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to get system serial number: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
productName, err = sysProductName()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to get system product name: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manufacturer, err = sysManufacturer()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to get system manufacturer: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialNumber, productName, manufacturer
|
||||||
|
}
|
||||||
|
|
||||||
func getOSNameAndVersion() (string, string) {
|
func getOSNameAndVersion() (string, string) {
|
||||||
var dst []Win32_OperatingSystem
|
var dst []Win32_OperatingSystem
|
||||||
query := wmi.CreateQuery(&dst, "")
|
query := wmi.CreateQuery(&dst, "")
|
||||||
|
|||||||
46
client/system/static_info.go
Normal file
46
client/system/static_info.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
//go:build (linux && !android) || windows || (darwin && !ios)
|
||||||
|
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/system/detect_cloud"
|
||||||
|
"github.com/netbirdio/netbird/client/system/detect_platform"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
staticInfo StaticInfo
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go func() {
|
||||||
|
_ = updateStaticInfo()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateStaticInfo() StaticInfo {
|
||||||
|
once.Do(func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(3)
|
||||||
|
go func() {
|
||||||
|
staticInfo.SystemSerialNumber, staticInfo.SystemProductName, staticInfo.SystemManufacturer = sysInfo()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
staticInfo.Environment.Cloud = detect_cloud.Detect(ctx)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
staticInfo.Environment.Platform = detect_platform.Detect(ctx)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
})
|
||||||
|
return staticInfo
|
||||||
|
}
|
||||||
@@ -183,7 +183,10 @@ func Test_sysInfo(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
gotSerialNum, gotProdName, gotManufacturer := sysInfo(tt.sysInfo)
|
getSystemInfo = func() SysInfo {
|
||||||
|
return tt.sysInfo
|
||||||
|
}
|
||||||
|
gotSerialNum, gotProdName, gotManufacturer := sysInfo()
|
||||||
if gotSerialNum != tt.wantSerialNum {
|
if gotSerialNum != tt.wantSerialNum {
|
||||||
t.Errorf("sysInfo() gotSerialNum = %v, want %v", gotSerialNum, tt.wantSerialNum)
|
t.Errorf("sysInfo() gotSerialNum = %v, want %v", gotSerialNum, tt.wantSerialNum)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user