mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-20 01:06:45 +00:00
Compare commits
246 Commits
feature/op
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb77ff4661 | ||
|
|
83b83ccfd2 | ||
|
|
4e75e15ea1 | ||
|
|
06afe64aff | ||
|
|
7acaef1152 | ||
|
|
469be3442d | ||
|
|
d1b6387803 | ||
|
|
820e2feec9 | ||
|
|
e0d086a8a8 | ||
|
|
1f95467b02 | ||
|
|
6553d8ce03 | ||
|
|
f0c829afac | ||
|
|
86f14523e4 | ||
|
|
6cefcbfe5d | ||
|
|
19103031ee | ||
|
|
7369f4bc38 | ||
|
|
d9d275a7ce | ||
|
|
57b85f4f8d | ||
|
|
7ef191903e | ||
|
|
3bd15dd1c4 | ||
|
|
1065e0a6c5 | ||
|
|
d4ff55e6fe | ||
|
|
5625d83c3f | ||
|
|
63f2f51614 | ||
|
|
defdcb631e | ||
|
|
7bf0d04bed | ||
|
|
e4ec1fd757 | ||
|
|
dab50f35d7 | ||
|
|
2d7e797e08 | ||
|
|
c3e8187a47 | ||
|
|
cfac8c4762 | ||
|
|
d9dfae625b | ||
|
|
a9e6742d9a | ||
|
|
931f165c9a | ||
|
|
2803e1669b | ||
|
|
f28a657a1d | ||
|
|
1f949f8cee | ||
|
|
75f5b75bc4 | ||
|
|
48a2f6e69d | ||
|
|
b3715b5fad | ||
|
|
836072098b | ||
|
|
85e991ff78 | ||
|
|
f9845e53a0 | ||
|
|
8845e8fbc7 | ||
|
|
1a5ee744a8 | ||
|
|
765aba2c1c | ||
|
|
7cb81f1d70 | ||
|
|
cea19de667 | ||
|
|
29e5eceb6b | ||
|
|
0f63737330 | ||
|
|
bf518c5fba | ||
|
|
eab6183a8e | ||
|
|
15a7b7629b | ||
|
|
4517da8b3a | ||
|
|
9c0d923124 | ||
|
|
6857734c48 | ||
|
|
d3785dc1fa | ||
|
|
ed82ef7fe4 | ||
|
|
aa55fba5ee | ||
|
|
faeae52329 | ||
|
|
9ae03046e7 | ||
|
|
98aa830831 | ||
|
|
c94c949173 | ||
|
|
183f746158 | ||
|
|
dd0d15c9d4 | ||
|
|
3b019800f8 | ||
|
|
4cd4f88666 | ||
|
|
4d0e16f2d0 | ||
|
|
3fcdb51376 | ||
|
|
c0efce6556 | ||
|
|
f0eb004582 | ||
|
|
0a59f12012 | ||
|
|
745e4f76b1 | ||
|
|
085d072b17 | ||
|
|
d2157bda66 | ||
|
|
0a67f5be1a | ||
|
|
43a8ba97e3 | ||
|
|
17874771cc | ||
|
|
f6ccf6b97a | ||
|
|
6aae797baf | ||
|
|
aca054e51e | ||
|
|
f72e852ccb | ||
|
|
10cee8f46e | ||
|
|
54dc78aab8 | ||
|
|
628673db20 | ||
|
|
eaa31c2dc6 | ||
|
|
25723e9b07 | ||
|
|
3cf4d5758f | ||
|
|
69d8d5aa86 | ||
|
|
7581bbd925 | ||
|
|
4d67d72785 | ||
|
|
4a08f1a1e9 | ||
|
|
bfe60c01ba | ||
|
|
06ceac65de | ||
|
|
fc15ee6351 | ||
|
|
6801dcb3f6 | ||
|
|
4a3e78fb0f | ||
|
|
f9462eea27 | ||
|
|
b075009ef7 | ||
|
|
c7db2c0524 | ||
|
|
4f890ff712 | ||
|
|
f7e6aa9b8f | ||
|
|
81f2330d49 | ||
|
|
0261e15aad | ||
|
|
c347a4c2ca | ||
|
|
11de2ec42e | ||
|
|
61bc092458 | ||
|
|
4d2a25b728 | ||
|
|
2f32e0d8cf | ||
|
|
b679404618 | ||
|
|
48310ef99c | ||
|
|
24f71bc68a | ||
|
|
e26e2c3a75 | ||
|
|
215fb257f7 | ||
|
|
381447b8d6 | ||
|
|
919c1cb3d4 | ||
|
|
85d17cbc89 | ||
|
|
a5e664d83d | ||
|
|
d8ab3c1632 | ||
|
|
63b4041e9c | ||
|
|
f7d8d03e55 | ||
|
|
5b86a7f3f2 | ||
|
|
deb8203f06 | ||
|
|
e407fe02c5 | ||
|
|
c9f3854dde | ||
|
|
a7760bf0a7 | ||
|
|
64f949abbb | ||
|
|
245b086646 | ||
|
|
1609b21b5b | ||
|
|
1f926d15b8 | ||
|
|
a432e8e23a | ||
|
|
4fec709bb1 | ||
|
|
95299be52d | ||
|
|
f51cae7103 | ||
|
|
f68d5e965f | ||
|
|
85b8f36ec1 | ||
|
|
38f2a59d1b | ||
|
|
94e505480b | ||
|
|
9504012920 | ||
|
|
5e93d117cf | ||
|
|
8c70b7d7ff | ||
|
|
ed8def4d9b | ||
|
|
10d8617be6 | ||
|
|
1e115e3893 | ||
|
|
deffe037aa | ||
|
|
fed9e587af | ||
|
|
983d7bafbe | ||
|
|
a40d4d2f32 | ||
|
|
4da29451d0 | ||
|
|
15818b72c6 | ||
|
|
0556dc1860 | ||
|
|
2b369cd28f | ||
|
|
9d44a476c6 | ||
|
|
9b3449753e | ||
|
|
456629811b | ||
|
|
57ddb5f262 | ||
|
|
4ced07dd8d | ||
|
|
3430b81622 | ||
|
|
fd4ad15c83 | ||
|
|
c311d0d19e | ||
|
|
521f7dd39f | ||
|
|
f9ec0a9a2e | ||
|
|
012235ff12 | ||
|
|
4ff069a102 | ||
|
|
7cc3964a4d | ||
|
|
6d627f1923 | ||
|
|
076ce69a24 | ||
|
|
f176807ebe | ||
|
|
d4c47eaf8a | ||
|
|
645a1f31a7 | ||
|
|
b4aa7e50f9 | ||
|
|
d35a79d3b5 | ||
|
|
6a2929011d | ||
|
|
173ca25dac | ||
|
|
e877c9d6c1 | ||
|
|
7a1c96ebf4 | ||
|
|
41fe9f84ec | ||
|
|
d13fb0e379 | ||
|
|
f3214527ea | ||
|
|
69048bfd34 | ||
|
|
29a2d93873 | ||
|
|
6b01b0020e | ||
|
|
9d3db68805 | ||
|
|
2e315311e0 | ||
|
|
36b2cd16cc | ||
|
|
67e2185964 | ||
|
|
89149dc6f4 | ||
|
|
5a1f8f13a2 | ||
|
|
e71059d245 | ||
|
|
91fa2e20a0 | ||
|
|
61034aaf4d | ||
|
|
0a05f8b4d4 | ||
|
|
e82c0a55a3 | ||
|
|
13eb457132 | ||
|
|
b8717b8956 | ||
|
|
1c9c9ae47e | ||
|
|
9ac5a1ed3f | ||
|
|
d4eaec5cbd | ||
|
|
6ae7a790f2 | ||
|
|
49dfbc82d9 | ||
|
|
57a89cf0cc | ||
|
|
50201d63c2 | ||
|
|
d11b39282b | ||
|
|
bd58eea8ea | ||
|
|
a5811a2d7d | ||
|
|
a680f80ed9 | ||
|
|
10fbdc2c4a | ||
|
|
1444fbe104 | ||
|
|
650bca7ca8 | ||
|
|
570e28d227 | ||
|
|
272ade07a8 | ||
|
|
263abe4862 | ||
|
|
ceee421a05 | ||
|
|
0a75da6fb7 | ||
|
|
920877964f | ||
|
|
2e0047daea | ||
|
|
ce0718fcb5 | ||
|
|
c590518e0c | ||
|
|
f309b120cd | ||
|
|
7357a9954c | ||
|
|
13b63eebc1 | ||
|
|
735ed7ab34 | ||
|
|
961d9198ef | ||
|
|
df4ca01848 | ||
|
|
4e7c17756c | ||
|
|
6a4935139d | ||
|
|
35dd991776 | ||
|
|
3598418206 | ||
|
|
e435e39158 | ||
|
|
fd26e989e3 | ||
|
|
4424162bce | ||
|
|
54b045d9ca | ||
|
|
71c6437bab | ||
|
|
7b254cb966 | ||
|
|
8f3a0f2c38 | ||
|
|
1f33e2e003 | ||
|
|
1e6addaa65 | ||
|
|
f51dc13f8c | ||
|
|
3477108ce7 | ||
|
|
012e624296 | ||
|
|
4c5e987e02 | ||
|
|
a80c8b0176 | ||
|
|
9e01155d2e | ||
|
|
3c3111ad01 | ||
|
|
b74078fd95 | ||
|
|
77488ad11a |
2
.github/workflows/golang-test-darwin.yml
vendored
2
.github/workflows/golang-test-darwin.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
store: ['jsonfile', 'sqlite']
|
store: ['sqlite']
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
|
|||||||
39
.github/workflows/golang-test-freebsd.yml
vendored
Normal file
39
.github/workflows/golang-test-freebsd.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
name: Test Code FreeBSD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Test in FreeBSD
|
||||||
|
id: test
|
||||||
|
uses: vmactions/freebsd-vm@v1
|
||||||
|
with:
|
||||||
|
usesh: true
|
||||||
|
prepare: |
|
||||||
|
pkg install -y curl
|
||||||
|
pkg install -y git
|
||||||
|
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
curl -o go.tar.gz https://go.dev/dl/go1.21.11.freebsd-amd64.tar.gz -L
|
||||||
|
tar zxf go.tar.gz
|
||||||
|
mv go /usr/local/go
|
||||||
|
ln -s /usr/local/go/bin/go /usr/local/bin/go
|
||||||
|
go mod tidy
|
||||||
|
go test -timeout 5m -p 1 ./iface/...
|
||||||
|
go test -timeout 5m -p 1 ./client/...
|
||||||
|
cd client
|
||||||
|
go build .
|
||||||
|
cd ..
|
||||||
10
.github/workflows/golang-test-linux.yml
vendored
10
.github/workflows/golang-test-linux.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ '386','amd64' ]
|
arch: [ '386','amd64' ]
|
||||||
store: [ 'jsonfile', 'sqlite' ]
|
store: [ 'sqlite', 'postgres']
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
@@ -86,7 +86,10 @@ jobs:
|
|||||||
run: CGO_ENABLED=0 go test -c -o sharedsock-testing.bin ./sharedsock
|
run: CGO_ENABLED=0 go test -c -o sharedsock-testing.bin ./sharedsock
|
||||||
|
|
||||||
- name: Generate RouteManager Test bin
|
- name: Generate RouteManager Test bin
|
||||||
run: CGO_ENABLED=1 go test -c -o routemanager-testing.bin -tags netgo -ldflags '-w -extldflags "-static -ldbus-1 -lpcap"' ./client/internal/routemanager/...
|
run: CGO_ENABLED=0 go test -c -o routemanager-testing.bin ./client/internal/routemanager
|
||||||
|
|
||||||
|
- name: Generate SystemOps Test bin
|
||||||
|
run: CGO_ENABLED=1 go test -c -o systemops-testing.bin -tags netgo -ldflags '-w -extldflags "-static -ldbus-1 -lpcap"' ./client/internal/routemanager/systemops
|
||||||
|
|
||||||
- name: Generate nftables Manager Test bin
|
- name: Generate nftables Manager Test bin
|
||||||
run: CGO_ENABLED=0 go test -c -o nftablesmanager-testing.bin ./client/firewall/nftables/...
|
run: CGO_ENABLED=0 go test -c -o nftablesmanager-testing.bin ./client/firewall/nftables/...
|
||||||
@@ -108,6 +111,9 @@ jobs:
|
|||||||
- name: Run RouteManager tests in docker
|
- name: Run RouteManager tests in docker
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/routemanager-testing.bin -test.timeout 5m -test.parallel 1
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/routemanager-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
|
|
||||||
|
- name: Run SystemOps tests in docker
|
||||||
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager/systemops --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/systemops-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
|
|
||||||
- name: Run nftables Manager tests in docker
|
- name: Run nftables Manager tests in docker
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/firewall --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/nftablesmanager-testing.bin -test.timeout 5m -test.parallel 1
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/firewall --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/nftablesmanager-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/golangci-lint.yml
vendored
2
.github/workflows/golangci-lint.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- name: codespell
|
- name: codespell
|
||||||
uses: codespell-project/actions-codespell@v2
|
uses: codespell-project/actions-codespell@v2
|
||||||
with:
|
with:
|
||||||
ignore_words_list: erro,clienta
|
ignore_words_list: erro,clienta,hastable,
|
||||||
skip: go.mod,go.sum
|
skip: go.mod,go.sum
|
||||||
only_warn: 1
|
only_warn: 1
|
||||||
golangci:
|
golangci:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
- name: Setup NDK
|
- name: Setup NDK
|
||||||
run: /usr/local/lib/android/sdk/cmdline-tools/7.0/bin/sdkmanager --install "ndk;23.1.7779620"
|
run: /usr/local/lib/android/sdk/cmdline-tools/7.0/bin/sdkmanager --install "ndk;23.1.7779620"
|
||||||
- name: install gomobile
|
- name: install gomobile
|
||||||
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20230531173138-3c911d8e3eda
|
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240404231514-09dbf07665ed
|
||||||
- name: gomobile init
|
- name: gomobile init
|
||||||
run: gomobile init
|
run: gomobile init
|
||||||
- name: build android netbird lib
|
- name: build android netbird lib
|
||||||
@@ -56,10 +56,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: "1.21.x"
|
go-version: "1.21.x"
|
||||||
- name: install gomobile
|
- name: install gomobile
|
||||||
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20230531173138-3c911d8e3eda
|
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240404231514-09dbf07665ed
|
||||||
- name: gomobile init
|
- name: gomobile init
|
||||||
run: gomobile init
|
run: gomobile init
|
||||||
- name: build iOS netbird lib
|
- name: build iOS netbird lib
|
||||||
run: PATH=$PATH:$(go env GOPATH) gomobile bind -target=ios -bundleid=io.netbird.framework -ldflags="-X github.com/netbirdio/netbird/version.version=buildtest" -o $GITHUB_WORKSPACE/NetBirdSDK.xcframework $GITHUB_WORKSPACE/client/ios/NetBirdSDK
|
run: PATH=$PATH:$(go env GOPATH) gomobile bind -target=ios -bundleid=io.netbird.framework -ldflags="-X github.com/netbirdio/netbird/version.version=buildtest" -o ./NetBirdSDK.xcframework ./client/ios/NetBirdSDK
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
35
.github/workflows/release.yml
vendored
35
.github/workflows/release.yml
vendored
@@ -7,17 +7,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
|
||||||
- 'go.mod'
|
|
||||||
- 'go.sum'
|
|
||||||
- '.goreleaser.yml'
|
|
||||||
- '.goreleaser_ui.yaml'
|
|
||||||
- '.goreleaser_ui_darwin.yaml'
|
|
||||||
- '.github/workflows/release.yml'
|
|
||||||
- 'release_files/**'
|
|
||||||
- '**/Dockerfile'
|
|
||||||
- '**/Dockerfile.*'
|
|
||||||
- 'client/ui/**'
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.0.11"
|
SIGN_PIPE_VER: "v0.0.11"
|
||||||
@@ -106,6 +96,27 @@ jobs:
|
|||||||
name: release
|
name: release
|
||||||
path: dist/
|
path: dist/
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
-
|
||||||
|
name: upload linux packages
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: linux-packages
|
||||||
|
path: dist/netbird_linux**
|
||||||
|
retention-days: 3
|
||||||
|
-
|
||||||
|
name: upload windows packages
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: windows-packages
|
||||||
|
path: dist/netbird_windows**
|
||||||
|
retention-days: 3
|
||||||
|
-
|
||||||
|
name: upload macos packages
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: macos-packages
|
||||||
|
path: dist/netbird_darwin**
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
release_ui:
|
release_ui:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -162,7 +173,7 @@ jobs:
|
|||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
|
||||||
release_ui_darwin:
|
release_ui_darwin:
|
||||||
runs-on: macos-11
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
|
|||||||
59
.github/workflows/test-infrastructure-files.yml
vendored
59
.github/workflows/test-infrastructure-files.yml
vendored
@@ -178,34 +178,79 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: run script
|
- name: run script with Zitadel PostgreSQL
|
||||||
run: NETBIRD_DOMAIN=use-ip bash -x infrastructure_files/getting-started-with-zitadel.sh
|
run: NETBIRD_DOMAIN=use-ip bash -x infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
|
||||||
- name: test Caddy file gen
|
- name: test Caddy file gen postgres
|
||||||
run: test -f Caddyfile
|
run: test -f Caddyfile
|
||||||
- name: test docker-compose file gen
|
|
||||||
|
- name: test docker-compose file gen postgres
|
||||||
run: test -f docker-compose.yml
|
run: test -f docker-compose.yml
|
||||||
- name: test management.json file gen
|
|
||||||
|
- name: test management.json file gen postgres
|
||||||
run: test -f management.json
|
run: test -f management.json
|
||||||
- name: test turnserver.conf file gen
|
|
||||||
|
- name: test turnserver.conf file gen postgres
|
||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
test -f turnserver.conf
|
test -f turnserver.conf
|
||||||
grep external-ip turnserver.conf
|
grep external-ip turnserver.conf
|
||||||
- name: test zitadel.env file gen
|
|
||||||
|
- name: test zitadel.env file gen postgres
|
||||||
run: test -f zitadel.env
|
run: test -f zitadel.env
|
||||||
- name: test dashboard.env file gen
|
|
||||||
|
- name: test dashboard.env file gen postgres
|
||||||
run: test -f dashboard.env
|
run: test -f dashboard.env
|
||||||
|
|
||||||
|
- name: test zdb.env file gen postgres
|
||||||
|
run: test -f zdb.env
|
||||||
|
|
||||||
|
- name: Postgres run cleanup
|
||||||
|
run: |
|
||||||
|
docker-compose down --volumes --rmi all
|
||||||
|
rm -rf docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json zdb.env
|
||||||
|
|
||||||
|
- name: run script with Zitadel CockroachDB
|
||||||
|
run: bash -x infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
env:
|
||||||
|
NETBIRD_DOMAIN: use-ip
|
||||||
|
ZITADEL_DATABASE: cockroach
|
||||||
|
|
||||||
|
- name: test Caddy file gen CockroachDB
|
||||||
|
run: test -f Caddyfile
|
||||||
|
|
||||||
|
- name: test docker-compose file gen CockroachDB
|
||||||
|
run: test -f docker-compose.yml
|
||||||
|
|
||||||
|
- name: test management.json file gen CockroachDB
|
||||||
|
run: test -f management.json
|
||||||
|
|
||||||
|
- name: test turnserver.conf file gen CockroachDB
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
test -f turnserver.conf
|
||||||
|
grep external-ip turnserver.conf
|
||||||
|
|
||||||
|
- name: test zitadel.env file gen CockroachDB
|
||||||
|
run: test -f zitadel.env
|
||||||
|
|
||||||
|
- name: test dashboard.env file gen CockroachDB
|
||||||
|
run: test -f dashboard.env
|
||||||
|
|
||||||
test-download-geolite2-script:
|
test-download-geolite2-script:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install jq
|
- name: Install jq
|
||||||
run: sudo apt-get update && sudo apt-get install -y unzip sqlite3
|
run: sudo apt-get update && sudo apt-get install -y unzip sqlite3
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: test script
|
- name: test script
|
||||||
run: bash -x infrastructure_files/download-geolite2.sh
|
run: bash -x infrastructure_files/download-geolite2.sh
|
||||||
|
|
||||||
- name: test mmdb file exists
|
- name: test mmdb file exists
|
||||||
run: test -f GeoLite2-City.mmdb
|
run: test -f GeoLite2-City.mmdb
|
||||||
|
|
||||||
- name: test geonames file exists
|
- name: test geonames file exists
|
||||||
run: test -f geonames.db
|
run: test -f geonames.db
|
||||||
|
|||||||
@@ -130,3 +130,10 @@ issues:
|
|||||||
- path: mock\.go
|
- path: mock\.go
|
||||||
linters:
|
linters:
|
||||||
- nilnil
|
- nilnil
|
||||||
|
# Exclude specific deprecation warnings for grpc methods
|
||||||
|
- linters:
|
||||||
|
- staticcheck
|
||||||
|
text: "grpc.DialContext is deprecated"
|
||||||
|
- linters:
|
||||||
|
- staticcheck
|
||||||
|
text: "grpc.WithBlock is deprecated"
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ builds:
|
|||||||
- id: netbird-ui-darwin
|
- id: netbird-ui-darwin
|
||||||
dir: client/ui
|
dir: client/ui
|
||||||
binary: netbird-ui
|
binary: netbird-ui
|
||||||
env: [CGO_ENABLED=1]
|
env:
|
||||||
|
- CGO_ENABLED=1
|
||||||
|
- MACOSX_DEPLOYMENT_TARGET=11.0
|
||||||
|
- MACOS_DEPLOYMENT_TARGET=11.0
|
||||||
goos:
|
goos:
|
||||||
- darwin
|
- darwin
|
||||||
goarch:
|
goarch:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
We as members, contributors, and leaders pledge to make participation in our
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
identity and expression, level of experience, education, socioeconomic status,
|
||||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
identity and orientation.
|
identity and orientation.
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,8 @@
|
|||||||
|
|
||||||
### Open-Source Network Security in a Single Platform
|
### Open-Source Network Security in a Single Platform
|
||||||
|
|
||||||

|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
### Key features
|
### Key features
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
FROM alpine:3.18.5
|
FROM alpine:3.19
|
||||||
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 [ "/go/bin/netbird","up"]
|
ENTRYPOINT [ "/usr/local/bin/netbird","up"]
|
||||||
COPY netbird /go/bin/netbird
|
COPY netbird /usr/local/bin/netbird
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build android
|
||||||
|
|
||||||
package android
|
package android
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -14,6 +16,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
"github.com/netbirdio/netbird/formatter"
|
"github.com/netbirdio/netbird/formatter"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnectionListener export internal Listener for mobile
|
// ConnectionListener export internal Listener for mobile
|
||||||
@@ -54,14 +57,17 @@ type Client struct {
|
|||||||
ctxCancel context.CancelFunc
|
ctxCancel context.CancelFunc
|
||||||
ctxCancelLock *sync.Mutex
|
ctxCancelLock *sync.Mutex
|
||||||
deviceName string
|
deviceName string
|
||||||
|
uiVersion string
|
||||||
networkChangeListener listener.NetworkChangeListener
|
networkChangeListener listener.NetworkChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient instantiate a new Client
|
// NewClient instantiate a new Client
|
||||||
func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, networkChangeListener NetworkChangeListener) *Client {
|
func NewClient(cfgFile, deviceName string, uiVersion string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, networkChangeListener NetworkChangeListener) *Client {
|
||||||
|
net.SetAndroidProtectSocketFn(tunAdapter.ProtectSocket)
|
||||||
return &Client{
|
return &Client{
|
||||||
cfgFile: cfgFile,
|
cfgFile: cfgFile,
|
||||||
deviceName: deviceName,
|
deviceName: deviceName,
|
||||||
|
uiVersion: uiVersion,
|
||||||
tunAdapter: tunAdapter,
|
tunAdapter: tunAdapter,
|
||||||
iFaceDiscover: iFaceDiscover,
|
iFaceDiscover: iFaceDiscover,
|
||||||
recorder: peer.NewRecorder(""),
|
recorder: peer.NewRecorder(""),
|
||||||
@@ -84,6 +90,9 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead
|
|||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
//nolint
|
//nolint
|
||||||
ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
|
ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
|
||||||
|
//nolint
|
||||||
|
ctxWithValues = context.WithValue(ctxWithValues, system.UiVersionCtxKey, c.uiVersion)
|
||||||
|
|
||||||
c.ctxCancelLock.Lock()
|
c.ctxCancelLock.Lock()
|
||||||
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
|
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
|
||||||
defer c.ctxCancel()
|
defer c.ctxCancel()
|
||||||
@@ -97,7 +106,8 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead
|
|||||||
|
|
||||||
// todo do not throw error in case of cancelled context
|
// todo do not throw error in case of cancelled context
|
||||||
ctx = internal.CtxInitState(ctx)
|
ctx = internal.CtxInitState(ctx)
|
||||||
return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener)
|
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder)
|
||||||
|
return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
|
// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
|
||||||
@@ -122,7 +132,8 @@ func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener
|
|||||||
|
|
||||||
// todo do not throw error in case of cancelled context
|
// todo do not throw error in case of cancelled context
|
||||||
ctx = internal.CtxInitState(ctx)
|
ctx = internal.CtxInitState(ctx)
|
||||||
return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener)
|
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder)
|
||||||
|
return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the internal client and free the resources
|
// Stop the internal client and free the resources
|
||||||
|
|||||||
212
client/anonymize/anonymize.go
Normal file
212
client/anonymize/anonymize.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package anonymize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Anonymizer struct {
|
||||||
|
ipAnonymizer map[netip.Addr]netip.Addr
|
||||||
|
domainAnonymizer map[string]string
|
||||||
|
currentAnonIPv4 netip.Addr
|
||||||
|
currentAnonIPv6 netip.Addr
|
||||||
|
startAnonIPv4 netip.Addr
|
||||||
|
startAnonIPv6 netip.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultAddresses() (netip.Addr, netip.Addr) {
|
||||||
|
// 192.51.100.0, 100::
|
||||||
|
return netip.AddrFrom4([4]byte{198, 51, 100, 0}), netip.AddrFrom16([16]byte{0x01})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnonymizer(startIPv4, startIPv6 netip.Addr) *Anonymizer {
|
||||||
|
return &Anonymizer{
|
||||||
|
ipAnonymizer: map[netip.Addr]netip.Addr{},
|
||||||
|
domainAnonymizer: map[string]string{},
|
||||||
|
currentAnonIPv4: startIPv4,
|
||||||
|
currentAnonIPv6: startIPv6,
|
||||||
|
startAnonIPv4: startIPv4,
|
||||||
|
startAnonIPv6: startIPv6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Anonymizer) AnonymizeIP(ip netip.Addr) netip.Addr {
|
||||||
|
if ip.IsLoopback() ||
|
||||||
|
ip.IsLinkLocalUnicast() ||
|
||||||
|
ip.IsLinkLocalMulticast() ||
|
||||||
|
ip.IsInterfaceLocalMulticast() ||
|
||||||
|
ip.IsPrivate() ||
|
||||||
|
ip.IsUnspecified() ||
|
||||||
|
ip.IsMulticast() ||
|
||||||
|
isWellKnown(ip) ||
|
||||||
|
a.isInAnonymizedRange(ip) {
|
||||||
|
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := a.ipAnonymizer[ip]; !ok {
|
||||||
|
if ip.Is4() {
|
||||||
|
a.ipAnonymizer[ip] = a.currentAnonIPv4
|
||||||
|
a.currentAnonIPv4 = a.currentAnonIPv4.Next()
|
||||||
|
} else {
|
||||||
|
a.ipAnonymizer[ip] = a.currentAnonIPv6
|
||||||
|
a.currentAnonIPv6 = a.currentAnonIPv6.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.ipAnonymizer[ip]
|
||||||
|
}
|
||||||
|
|
||||||
|
// isInAnonymizedRange checks if an IP is within the range of already assigned anonymized IPs
|
||||||
|
func (a *Anonymizer) isInAnonymizedRange(ip netip.Addr) bool {
|
||||||
|
if ip.Is4() && ip.Compare(a.startAnonIPv4) >= 0 && ip.Compare(a.currentAnonIPv4) <= 0 {
|
||||||
|
return true
|
||||||
|
} else if !ip.Is4() && ip.Compare(a.startAnonIPv6) >= 0 && ip.Compare(a.currentAnonIPv6) <= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Anonymizer) AnonymizeIPString(ip string) string {
|
||||||
|
addr, err := netip.ParseAddr(ip)
|
||||||
|
if err != nil {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.AnonymizeIP(addr).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Anonymizer) AnonymizeDomain(domain string) string {
|
||||||
|
if strings.HasSuffix(domain, "netbird.io") ||
|
||||||
|
strings.HasSuffix(domain, "netbird.selfhosted") ||
|
||||||
|
strings.HasSuffix(domain, "netbird.cloud") ||
|
||||||
|
strings.HasSuffix(domain, "netbird.stage") ||
|
||||||
|
strings.HasSuffix(domain, ".domain") {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(domain, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDomain := parts[len(parts)-2] + "." + parts[len(parts)-1]
|
||||||
|
|
||||||
|
anonymized, ok := a.domainAnonymizer[baseDomain]
|
||||||
|
if !ok {
|
||||||
|
anonymizedBase := "anon-" + generateRandomString(5) + ".domain"
|
||||||
|
a.domainAnonymizer[baseDomain] = anonymizedBase
|
||||||
|
anonymized = anonymizedBase
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Replace(domain, baseDomain, anonymized, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Anonymizer) AnonymizeURI(uri string) string {
|
||||||
|
u, err := url.Parse(uri)
|
||||||
|
if err != nil {
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
var anonymizedHost string
|
||||||
|
if u.Opaque != "" {
|
||||||
|
host, port, err := net.SplitHostPort(u.Opaque)
|
||||||
|
if err == nil {
|
||||||
|
anonymizedHost = fmt.Sprintf("%s:%s", a.AnonymizeDomain(host), port)
|
||||||
|
} else {
|
||||||
|
anonymizedHost = a.AnonymizeDomain(u.Opaque)
|
||||||
|
}
|
||||||
|
u.Opaque = anonymizedHost
|
||||||
|
} else if u.Host != "" {
|
||||||
|
host, port, err := net.SplitHostPort(u.Host)
|
||||||
|
if err == nil {
|
||||||
|
anonymizedHost = fmt.Sprintf("%s:%s", a.AnonymizeDomain(host), port)
|
||||||
|
} else {
|
||||||
|
anonymizedHost = a.AnonymizeDomain(u.Host)
|
||||||
|
}
|
||||||
|
u.Host = anonymizedHost
|
||||||
|
}
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Anonymizer) AnonymizeString(str string) string {
|
||||||
|
ipv4Regex := regexp.MustCompile(`\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b`)
|
||||||
|
ipv6Regex := regexp.MustCompile(`\b([0-9a-fA-F:]+:+[0-9a-fA-F]{0,4})(?:%[0-9a-zA-Z]+)?(?:\/[0-9]{1,3})?(?::[0-9]{1,5})?\b`)
|
||||||
|
|
||||||
|
str = ipv4Regex.ReplaceAllStringFunc(str, a.AnonymizeIPString)
|
||||||
|
str = ipv6Regex.ReplaceAllStringFunc(str, a.AnonymizeIPString)
|
||||||
|
|
||||||
|
for domain, anonDomain := range a.domainAnonymizer {
|
||||||
|
str = strings.ReplaceAll(str, domain, anonDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
str = a.AnonymizeSchemeURI(str)
|
||||||
|
str = a.AnonymizeDNSLogLine(str)
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnonymizeSchemeURI finds and anonymizes URIs with stun, stuns, turn, and turns schemes.
|
||||||
|
func (a *Anonymizer) AnonymizeSchemeURI(text string) string {
|
||||||
|
re := regexp.MustCompile(`(?i)\b(stuns?:|turns?:|https?://)\S+\b`)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
domainPattern := `dns\.Question{Name:"([^"]+)",`
|
||||||
|
domainRegex := regexp.MustCompile(domainPattern)
|
||||||
|
|
||||||
|
return domainRegex.ReplaceAllStringFunc(logEntry, func(match string) string {
|
||||||
|
parts := strings.Split(match, `"`)
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
domain := parts[1]
|
||||||
|
if strings.HasSuffix(domain, ".domain") {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
randomDomain := generateRandomString(10) + ".domain"
|
||||||
|
return strings.Replace(match, domain, randomDomain, 1)
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWellKnown(addr netip.Addr) bool {
|
||||||
|
wellKnown := []string{
|
||||||
|
"8.8.8.8", "8.8.4.4", // Google DNS IPv4
|
||||||
|
"2001:4860:4860::8888", "2001:4860:4860::8844", // Google DNS IPv6
|
||||||
|
"1.1.1.1", "1.0.0.1", // Cloudflare DNS IPv4
|
||||||
|
"2606:4700:4700::1111", "2606:4700:4700::1001", // Cloudflare DNS IPv6
|
||||||
|
"9.9.9.9", "149.112.112.112", // Quad9 DNS IPv4
|
||||||
|
"2620:fe::fe", "2620:fe::9", // Quad9 DNS IPv6
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(wellKnown, addr.String()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
cgnatRangeStart := netip.AddrFrom4([4]byte{100, 64, 0, 0})
|
||||||
|
cgnatRange := netip.PrefixFrom(cgnatRangeStart, 10)
|
||||||
|
|
||||||
|
return cgnatRange.Contains(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomString(length int) string {
|
||||||
|
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
result := make([]byte, length)
|
||||||
|
for i := range result {
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[i] = letters[num.Int64()]
|
||||||
|
}
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
223
client/anonymize/anonymize_test.go
Normal file
223
client/anonymize/anonymize_test.go
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
package anonymize_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/anonymize"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAnonymizeIP(t *testing.T) {
|
||||||
|
startIPv4 := netip.MustParseAddr("198.51.100.0")
|
||||||
|
startIPv6 := netip.MustParseAddr("100::")
|
||||||
|
anonymizer := anonymize.NewAnonymizer(startIPv4, startIPv6)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ip string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{"Well known", "8.8.8.8", "8.8.8.8"},
|
||||||
|
{"First Public IPv4", "1.2.3.4", "198.51.100.0"},
|
||||||
|
{"Second Public IPv4", "4.3.2.1", "198.51.100.1"},
|
||||||
|
{"Repeated IPv4", "1.2.3.4", "198.51.100.0"},
|
||||||
|
{"Private IPv4", "192.168.1.1", "192.168.1.1"},
|
||||||
|
{"First Public IPv6", "2607:f8b0:4005:805::200e", "100::"},
|
||||||
|
{"Second Public IPv6", "a::b", "100::1"},
|
||||||
|
{"Repeated IPv6", "2607:f8b0:4005:805::200e", "100::"},
|
||||||
|
{"Private IPv6", "fe80::1", "fe80::1"},
|
||||||
|
{"In Range IPv4", "198.51.100.2", "198.51.100.2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ip := netip.MustParseAddr(tc.ip)
|
||||||
|
anonymizedIP := anonymizer.AnonymizeIP(ip)
|
||||||
|
if anonymizedIP.String() != tc.expect {
|
||||||
|
t.Errorf("%s: expected %s, got %s", tc.name, tc.expect, anonymizedIP)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeDNSLogLine(t *testing.T) {
|
||||||
|
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}`
|
||||||
|
|
||||||
|
result := anonymizer.AnonymizeDNSLogLine(testLog)
|
||||||
|
require.NotEqual(t, testLog, result)
|
||||||
|
assert.NotContains(t, result, "example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeDomain(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
domain string
|
||||||
|
expectPattern string
|
||||||
|
shouldAnonymize bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"General Domain",
|
||||||
|
"example.com",
|
||||||
|
`^anon-[a-zA-Z0-9]+\.domain$`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Subdomain",
|
||||||
|
"sub.example.com",
|
||||||
|
`^sub\.anon-[a-zA-Z0-9]+\.domain$`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Protected Domain",
|
||||||
|
"netbird.io",
|
||||||
|
`^netbird\.io$`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := anonymizer.AnonymizeDomain(tc.domain)
|
||||||
|
if tc.shouldAnonymize {
|
||||||
|
assert.Regexp(t, tc.expectPattern, result, "The anonymized domain should match the expected pattern")
|
||||||
|
assert.NotContains(t, result, tc.domain, "The original domain should not be present in the result")
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tc.domain, result, "Protected domains should not be anonymized")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeURI(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uri string
|
||||||
|
regex string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"HTTP URI with Port",
|
||||||
|
"http://example.com:80/path",
|
||||||
|
`^http://anon-[a-zA-Z0-9]+\.domain:80/path$`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"HTTP URI without Port",
|
||||||
|
"http://example.com/path",
|
||||||
|
`^http://anon-[a-zA-Z0-9]+\.domain/path$`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Opaque URI with Port",
|
||||||
|
"stun:example.com:80?transport=udp",
|
||||||
|
`^stun:anon-[a-zA-Z0-9]+\.domain:80\?transport=udp$`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Opaque URI without Port",
|
||||||
|
"stun:example.com?transport=udp",
|
||||||
|
`^stun:anon-[a-zA-Z0-9]+\.domain\?transport=udp$`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := anonymizer.AnonymizeURI(tc.uri)
|
||||||
|
assert.Regexp(t, regexp.MustCompile(tc.regex), result, "URI should match expected pattern")
|
||||||
|
require.NotContains(t, result, "example.com", "Original domain should not be present")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeSchemeURI(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{"STUN URI in text", "Connection made via stun:example.com", `Connection made via stun:anon-[a-zA-Z0-9]+\.domain`},
|
||||||
|
{"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`},
|
||||||
|
{"HTTPS URI in message", "Visit https://example.com for more", `Visit https://anon-[a-zA-Z0-9]+\.domain for more`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := anonymizer.AnonymizeSchemeURI(tc.input)
|
||||||
|
assert.Regexp(t, tc.expect, result, "The anonymized output should match expected pattern")
|
||||||
|
require.NotContains(t, result, "example.com", "Original domain should not be present")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizString_MemorizedDomain(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||||
|
domain := "example.com"
|
||||||
|
anonymizedDomain := anonymizer.AnonymizeDomain(domain)
|
||||||
|
|
||||||
|
sampleString := "This is a test string including the domain example.com which should be anonymized."
|
||||||
|
|
||||||
|
firstPassResult := anonymizer.AnonymizeString(sampleString)
|
||||||
|
secondPassResult := anonymizer.AnonymizeString(firstPassResult)
|
||||||
|
|
||||||
|
assert.Contains(t, firstPassResult, anonymizedDomain, "The domain should be anonymized in the first pass")
|
||||||
|
assert.NotContains(t, firstPassResult, domain, "The original domain should not appear in the first pass output")
|
||||||
|
|
||||||
|
assert.Equal(t, firstPassResult, secondPassResult, "The second pass should not further anonymize the string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeString_DoubleURI(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||||
|
domain := "example.com"
|
||||||
|
anonymizedDomain := anonymizer.AnonymizeDomain(domain)
|
||||||
|
|
||||||
|
sampleString := "Check out our site at https://example.com for more info."
|
||||||
|
|
||||||
|
firstPassResult := anonymizer.AnonymizeString(sampleString)
|
||||||
|
secondPassResult := anonymizer.AnonymizeString(firstPassResult)
|
||||||
|
|
||||||
|
assert.Contains(t, firstPassResult, "https://"+anonymizedDomain, "The URI should be anonymized in the first pass")
|
||||||
|
assert.NotContains(t, firstPassResult, "https://example.com", "The original URI should not appear in the first pass output")
|
||||||
|
|
||||||
|
assert.Equal(t, firstPassResult, secondPassResult, "The second pass should not further anonymize the URI")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeString_IPAddresses(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "IPv4 Address",
|
||||||
|
input: "Error occurred at IP 122.138.1.1",
|
||||||
|
expect: "Error occurred at IP 198.51.100.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 Address",
|
||||||
|
input: "Access attempted from 2001:db8::ff00:42",
|
||||||
|
expect: "Access attempted from 100::",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 Address with Port",
|
||||||
|
input: "Access attempted from [2001:db8::ff00:42]:8080",
|
||||||
|
expect: "Access attempted from [100::]:8080",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Both IPv4 and IPv6",
|
||||||
|
input: "IPv4: 142.108.0.1 and IPv6: 2001:db8::ff00:43",
|
||||||
|
expect: "IPv4: 198.51.100.1 and IPv6: 100::1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := anonymizer.AnonymizeString(tc.input)
|
||||||
|
assert.Equal(t, tc.expect, result, "IP addresses should be anonymized correctly")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
255
client/cmd/debug.go
Normal file
255
client/cmd/debug.go
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
"github.com/netbirdio/netbird/client/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var debugCmd = &cobra.Command{
|
||||||
|
Use: "debug",
|
||||||
|
Short: "Debugging commands",
|
||||||
|
Long: "Provides commands for debugging and logging control within the Netbird daemon.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var debugBundleCmd = &cobra.Command{
|
||||||
|
Use: "bundle",
|
||||||
|
Example: " netbird debug bundle",
|
||||||
|
Short: "Create a debug bundle",
|
||||||
|
Long: "Generates a compressed archive of the daemon's logs and status for debugging purposes.",
|
||||||
|
RunE: debugBundle,
|
||||||
|
}
|
||||||
|
|
||||||
|
var logCmd = &cobra.Command{
|
||||||
|
Use: "log",
|
||||||
|
Short: "Manage logging for the Netbird daemon",
|
||||||
|
Long: `Commands to manage logging settings for the Netbird daemon, including ICE, gRPC, and general log levels.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var logLevelCmd = &cobra.Command{
|
||||||
|
Use: "level <level>",
|
||||||
|
Short: "Set the logging level for this session",
|
||||||
|
Long: `Sets the logging level for the current session. This setting is temporary and will revert to the default on daemon restart.
|
||||||
|
Available log levels are:
|
||||||
|
panic: for panic level, highest level of severity
|
||||||
|
fatal: for fatal level errors that cause the program to exit
|
||||||
|
error: for error conditions
|
||||||
|
warn: for warning conditions
|
||||||
|
info: for informational messages
|
||||||
|
debug: for debug-level messages
|
||||||
|
trace: for trace-level messages, which include more fine-grained information than debug`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: setLogLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
var forCmd = &cobra.Command{
|
||||||
|
Use: "for <time>",
|
||||||
|
Short: "Run debug logs for a specified duration and create a debug bundle",
|
||||||
|
Long: `Sets the logging level to trace, runs for the specified duration, and then generates a debug bundle.`,
|
||||||
|
Example: " netbird debug for 5m",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runForDuration,
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugBundle(cmd *cobra.Command, _ []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{
|
||||||
|
Anonymize: anonymizeFlag,
|
||||||
|
Status: getStatusOutput(cmd),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println(resp.GetPath())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setLogLevel(cmd *cobra.Command, args []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
level := server.ParseLogLevel(args[0])
|
||||||
|
if level == proto.LogLevel_UNKNOWN {
|
||||||
|
return fmt.Errorf("unknown log level: %s. Available levels are: panic, fatal, error, warn, info, debug, trace\n", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.SetLogLevel(cmd.Context(), &proto.SetLogLevelRequest{
|
||||||
|
Level: level,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set log level: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Log level set successfully to", args[0])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runForDuration(cmd *cobra.Command, args []string) error {
|
||||||
|
duration, err := time.ParseDuration(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid duration format: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
|
stat, err := client.Status(cmd.Context(), &proto.StatusRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get status: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreUp := stat.Status == string(internal.StatusConnected) || stat.Status == string(internal.StatusConnecting)
|
||||||
|
|
||||||
|
initialLogLevel, err := client.GetLogLevel(cmd.Context(), &proto.GetLogLevelRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get log level: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to down: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird down")
|
||||||
|
|
||||||
|
initialLevelTrace := initialLogLevel.GetLevel() >= proto.LogLevel_TRACE
|
||||||
|
if !initialLevelTrace {
|
||||||
|
_, err = client.SetLogLevel(cmd.Context(), &proto.SetLogLevelRequest{
|
||||||
|
Level: proto.LogLevel_TRACE,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set log level to TRACE: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
cmd.Println("Log level set to trace.")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to up: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird up")
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
headerPostUp := fmt.Sprintf("----- Netbird post-up - Timestamp: %s", time.Now().Format(time.RFC3339))
|
||||||
|
statusOutput := fmt.Sprintf("%s\n%s", headerPostUp, getStatusOutput(cmd))
|
||||||
|
|
||||||
|
if waitErr := waitForDurationOrCancel(cmd.Context(), duration, cmd); waitErr != nil {
|
||||||
|
return waitErr
|
||||||
|
}
|
||||||
|
cmd.Println("\nDuration completed")
|
||||||
|
|
||||||
|
headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration)
|
||||||
|
statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd))
|
||||||
|
|
||||||
|
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to down: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird down")
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
if restoreUp {
|
||||||
|
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to up: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird up")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !initialLevelTrace {
|
||||||
|
if _, err := client.SetLogLevel(cmd.Context(), &proto.SetLogLevelRequest{Level: initialLogLevel.GetLevel()}); err != nil {
|
||||||
|
return fmt.Errorf("failed to restore log level: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
cmd.Println("Log level restored to", initialLogLevel.GetLevel())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Creating debug bundle...")
|
||||||
|
|
||||||
|
resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{
|
||||||
|
Anonymize: anonymizeFlag,
|
||||||
|
Status: statusOutput,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println(resp.GetPath())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatusOutput(cmd *cobra.Command) string {
|
||||||
|
var statusOutputString string
|
||||||
|
statusResp, err := getStatus(cmd.Context())
|
||||||
|
if err != nil {
|
||||||
|
cmd.PrintErrf("Failed to get status: %v\n", err)
|
||||||
|
} else {
|
||||||
|
statusOutputString = parseToFullDetailSummary(convertToStatusOutputOverview(statusResp))
|
||||||
|
}
|
||||||
|
return statusOutputString
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForDurationOrCancel(ctx context.Context, duration time.Duration, cmd *cobra.Command) error {
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
elapsed := time.Since(startTime)
|
||||||
|
if elapsed >= duration {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remaining := duration - elapsed
|
||||||
|
cmd.Printf("\rRemaining time: %s", formatDuration(remaining))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-done:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDuration(d time.Duration) string {
|
||||||
|
d = d.Round(time.Second)
|
||||||
|
h := d / time.Hour
|
||||||
|
d %= time.Hour
|
||||||
|
m := d / time.Minute
|
||||||
|
d %= time.Minute
|
||||||
|
s := d / time.Second
|
||||||
|
return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
|
||||||
|
}
|
||||||
@@ -2,9 +2,10 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/netbirdio/netbird/util"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,11 @@ const (
|
|||||||
preSharedKeyFlag = "preshared-key"
|
preSharedKeyFlag = "preshared-key"
|
||||||
interfaceNameFlag = "interface-name"
|
interfaceNameFlag = "interface-name"
|
||||||
wireguardPortFlag = "wireguard-port"
|
wireguardPortFlag = "wireguard-port"
|
||||||
|
networkMonitorFlag = "network-monitor"
|
||||||
disableAutoConnectFlag = "disable-auto-connect"
|
disableAutoConnectFlag = "disable-auto-connect"
|
||||||
serverSSHAllowedFlag = "allow-server-ssh"
|
serverSSHAllowedFlag = "allow-server-ssh"
|
||||||
extraIFaceBlackListFlag = "extra-iface-blacklist"
|
extraIFaceBlackListFlag = "extra-iface-blacklist"
|
||||||
|
dnsRouteIntervalFlag = "dns-router-interval"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -62,10 +64,14 @@ var (
|
|||||||
serverSSHAllowed bool
|
serverSSHAllowed bool
|
||||||
interfaceName string
|
interfaceName string
|
||||||
wireguardPort uint16
|
wireguardPort uint16
|
||||||
|
networkMonitor bool
|
||||||
serviceName string
|
serviceName string
|
||||||
autoConnectDisabled bool
|
autoConnectDisabled bool
|
||||||
extraIFaceBlackList []string
|
extraIFaceBlackList []string
|
||||||
rootCmd = &cobra.Command{
|
anonymizeFlag bool
|
||||||
|
dnsRouteInterval time.Duration
|
||||||
|
|
||||||
|
rootCmd = &cobra.Command{
|
||||||
Use: "netbird",
|
Use: "netbird",
|
||||||
Short: "",
|
Short: "",
|
||||||
Long: "",
|
Long: "",
|
||||||
@@ -119,6 +125,8 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)")
|
rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)")
|
||||||
rootCmd.PersistentFlags().StringVar(&preSharedKey, preSharedKeyFlag, "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.")
|
rootCmd.PersistentFlags().StringVar(&preSharedKey, preSharedKeyFlag, "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.")
|
||||||
rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device")
|
rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device")
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(&anonymizeFlag, "anonymize", "A", false, "anonymize IP addresses and non-netbird.io domains in logs and status output")
|
||||||
|
|
||||||
rootCmd.AddCommand(serviceCmd)
|
rootCmd.AddCommand(serviceCmd)
|
||||||
rootCmd.AddCommand(upCmd)
|
rootCmd.AddCommand(upCmd)
|
||||||
rootCmd.AddCommand(downCmd)
|
rootCmd.AddCommand(downCmd)
|
||||||
@@ -126,8 +134,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(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)
|
||||||
|
routesCmd.AddCommand(routesSelectCmd, routesDeselectCmd)
|
||||||
|
|
||||||
|
debugCmd.AddCommand(debugBundleCmd)
|
||||||
|
debugCmd.AddCommand(logCmd)
|
||||||
|
logCmd.AddCommand(logLevelCmd)
|
||||||
|
debugCmd.AddCommand(forCmd)
|
||||||
|
|
||||||
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.`+
|
||||||
`You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+
|
`You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+
|
||||||
@@ -335,3 +355,17 @@ func migrateToNetbird(oldPath, newPath string) bool {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getClient(cmd *cobra.Command) (*grpc.ClientConn, error) {
|
||||||
|
SetFlagsFromEnvVars(rootCmd)
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to daemon error: %v\n"+
|
||||||
|
"If the daemon is not running please run: "+
|
||||||
|
"\nnetbird service install \nnetbird service start\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|||||||
174
client/cmd/route.go
Normal file
174
client/cmd/route.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var sshCmd = &cobra.Command{
|
var sshCmd = &cobra.Command{
|
||||||
Use: "ssh",
|
Use: "ssh [user@]host",
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return errors.New("requires a host argument")
|
return errors.New("requires a host argument")
|
||||||
@@ -94,7 +94,7 @@ func runSSH(ctx context.Context, addr string, pemKey []byte, cmd *cobra.Command)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
cmd.Printf("Error: %v\n", err)
|
cmd.Printf("Error: %v\n", err)
|
||||||
cmd.Printf("Couldn't connect. Please check the connection status or if the ssh server is enabled on the other peer" +
|
cmd.Printf("Couldn't connect. Please check the connection status or if the ssh server is enabled on the other peer" +
|
||||||
"You can verify the connection by running:\n\n" +
|
"\nYou can verify the connection by running:\n\n" +
|
||||||
" netbird status\n\n")
|
" netbird status\n\n")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -14,6 +16,7 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/anonymize"
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
@@ -144,9 +147,9 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("failed initializing log %v", err)
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := internal.CtxInitState(context.Background())
|
ctx := internal.CtxInitState(cmd.Context())
|
||||||
|
|
||||||
resp, err := getStatus(ctx, cmd)
|
resp, err := getStatus(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -191,7 +194,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStatus(ctx context.Context, cmd *cobra.Command) (*proto.StatusResponse, error) {
|
func getStatus(ctx context.Context) (*proto.StatusResponse, error) {
|
||||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to daemon error: %v\n"+
|
return nil, fmt.Errorf("failed to connect to daemon error: %v\n"+
|
||||||
@@ -200,7 +203,7 @@ func getStatus(ctx context.Context, cmd *cobra.Command) (*proto.StatusResponse,
|
|||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
resp, err := proto.NewDaemonServiceClient(conn).Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true})
|
resp, err := proto.NewDaemonServiceClient(conn).Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("status failed: %v", status.Convert(err).Message())
|
return nil, fmt.Errorf("status failed: %v", status.Convert(err).Message())
|
||||||
}
|
}
|
||||||
@@ -283,6 +286,11 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv
|
|||||||
NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()),
|
NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if anonymizeFlag {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||||
|
anonymizeOverview(anonymizer, &overview)
|
||||||
|
}
|
||||||
|
|
||||||
return overview
|
return overview
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,8 +533,16 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
|
|||||||
|
|
||||||
peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total)
|
peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total)
|
||||||
|
|
||||||
|
goos := runtime.GOOS
|
||||||
|
goarch := runtime.GOARCH
|
||||||
|
goarm := ""
|
||||||
|
if goarch == "arm" {
|
||||||
|
goarm = fmt.Sprintf(" (ARMv%s)", os.Getenv("GOARM"))
|
||||||
|
}
|
||||||
|
|
||||||
summary := fmt.Sprintf(
|
summary := fmt.Sprintf(
|
||||||
"Daemon version: %s\n"+
|
"OS: %s\n"+
|
||||||
|
"Daemon version: %s\n"+
|
||||||
"CLI version: %s\n"+
|
"CLI version: %s\n"+
|
||||||
"Management: %s\n"+
|
"Management: %s\n"+
|
||||||
"Signal: %s\n"+
|
"Signal: %s\n"+
|
||||||
@@ -538,6 +554,7 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
|
|||||||
"Quantum resistance: %s\n"+
|
"Quantum resistance: %s\n"+
|
||||||
"Routes: %s\n"+
|
"Routes: %s\n"+
|
||||||
"Peers count: %s\n",
|
"Peers count: %s\n",
|
||||||
|
fmt.Sprintf("%s/%s%s", goos, goarch, goarm),
|
||||||
overview.DaemonVersion,
|
overview.DaemonVersion,
|
||||||
version.NetbirdVersion(),
|
version.NetbirdVersion(),
|
||||||
managementConnString,
|
managementConnString,
|
||||||
@@ -593,15 +610,6 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
|
|||||||
if peerState.IceCandidateEndpoint.Remote != "" {
|
if peerState.IceCandidateEndpoint.Remote != "" {
|
||||||
remoteICEEndpoint = peerState.IceCandidateEndpoint.Remote
|
remoteICEEndpoint = peerState.IceCandidateEndpoint.Remote
|
||||||
}
|
}
|
||||||
lastStatusUpdate := "-"
|
|
||||||
if !peerState.LastStatusUpdate.IsZero() {
|
|
||||||
lastStatusUpdate = peerState.LastStatusUpdate.Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
|
|
||||||
lastWireGuardHandshake := "-"
|
|
||||||
if !peerState.LastWireguardHandshake.IsZero() && peerState.LastWireguardHandshake != time.Unix(0, 0) {
|
|
||||||
lastWireGuardHandshake = peerState.LastWireguardHandshake.Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
|
|
||||||
rosenpassEnabledStatus := "false"
|
rosenpassEnabledStatus := "false"
|
||||||
if rosenpassEnabled {
|
if rosenpassEnabled {
|
||||||
@@ -652,8 +660,8 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
|
|||||||
remoteICE,
|
remoteICE,
|
||||||
localICEEndpoint,
|
localICEEndpoint,
|
||||||
remoteICEEndpoint,
|
remoteICEEndpoint,
|
||||||
lastStatusUpdate,
|
timeAgo(peerState.LastStatusUpdate),
|
||||||
lastWireGuardHandshake,
|
timeAgo(peerState.LastWireguardHandshake),
|
||||||
toIEC(peerState.TransferReceived),
|
toIEC(peerState.TransferReceived),
|
||||||
toIEC(peerState.TransferSent),
|
toIEC(peerState.TransferSent),
|
||||||
rosenpassEnabledStatus,
|
rosenpassEnabledStatus,
|
||||||
@@ -722,3 +730,134 @@ func countEnabled(dnsServers []nsServerGroupStateOutput) int {
|
|||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// timeAgo returns a string representing the duration since the provided time in a human-readable format.
|
||||||
|
func timeAgo(t time.Time) string {
|
||||||
|
if t.IsZero() || t.Equal(time.Unix(0, 0)) {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
duration := time.Since(t)
|
||||||
|
switch {
|
||||||
|
case duration < time.Second:
|
||||||
|
return "Now"
|
||||||
|
case duration < time.Minute:
|
||||||
|
seconds := int(duration.Seconds())
|
||||||
|
if seconds == 1 {
|
||||||
|
return "1 second ago"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d seconds ago", seconds)
|
||||||
|
case duration < time.Hour:
|
||||||
|
minutes := int(duration.Minutes())
|
||||||
|
seconds := int(duration.Seconds()) % 60
|
||||||
|
if minutes == 1 {
|
||||||
|
if seconds == 1 {
|
||||||
|
return "1 minute, 1 second ago"
|
||||||
|
} else if seconds > 0 {
|
||||||
|
return fmt.Sprintf("1 minute, %d seconds ago", seconds)
|
||||||
|
}
|
||||||
|
return "1 minute ago"
|
||||||
|
}
|
||||||
|
if seconds > 0 {
|
||||||
|
return fmt.Sprintf("%d minutes, %d seconds ago", minutes, seconds)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d minutes ago", minutes)
|
||||||
|
case duration < 24*time.Hour:
|
||||||
|
hours := int(duration.Hours())
|
||||||
|
minutes := int(duration.Minutes()) % 60
|
||||||
|
if hours == 1 {
|
||||||
|
if minutes == 1 {
|
||||||
|
return "1 hour, 1 minute ago"
|
||||||
|
} else if minutes > 0 {
|
||||||
|
return fmt.Sprintf("1 hour, %d minutes ago", minutes)
|
||||||
|
}
|
||||||
|
return "1 hour ago"
|
||||||
|
}
|
||||||
|
if minutes > 0 {
|
||||||
|
return fmt.Sprintf("%d hours, %d minutes ago", hours, minutes)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d hours ago", hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
days := int(duration.Hours()) / 24
|
||||||
|
hours := int(duration.Hours()) % 24
|
||||||
|
if days == 1 {
|
||||||
|
if hours == 1 {
|
||||||
|
return "1 day, 1 hour ago"
|
||||||
|
} else if hours > 0 {
|
||||||
|
return fmt.Sprintf("1 day, %d hours ago", hours)
|
||||||
|
}
|
||||||
|
return "1 day ago"
|
||||||
|
}
|
||||||
|
if hours > 0 {
|
||||||
|
return fmt.Sprintf("%d days, %d hours ago", days, hours)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d days ago", days)
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizePeerDetail(a *anonymize.Anonymizer, peer *peerStateDetailOutput) {
|
||||||
|
peer.FQDN = a.AnonymizeDomain(peer.FQDN)
|
||||||
|
if localIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Local); err == nil {
|
||||||
|
peer.IceCandidateEndpoint.Local = fmt.Sprintf("%s:%s", a.AnonymizeIPString(localIP), port)
|
||||||
|
}
|
||||||
|
if remoteIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Remote); err == nil {
|
||||||
|
peer.IceCandidateEndpoint.Remote = fmt.Sprintf("%s:%s", a.AnonymizeIPString(remoteIP), port)
|
||||||
|
}
|
||||||
|
for i, route := range peer.Routes {
|
||||||
|
peer.Routes[i] = a.AnonymizeIPString(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, route := range peer.Routes {
|
||||||
|
peer.Routes[i] = anonymizeRoute(a, route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview) {
|
||||||
|
for i, peer := range overview.Peers.Details {
|
||||||
|
peer := peer
|
||||||
|
anonymizePeerDetail(a, &peer)
|
||||||
|
overview.Peers.Details[i] = peer
|
||||||
|
}
|
||||||
|
|
||||||
|
overview.ManagementState.URL = a.AnonymizeURI(overview.ManagementState.URL)
|
||||||
|
overview.ManagementState.Error = a.AnonymizeString(overview.ManagementState.Error)
|
||||||
|
overview.SignalState.URL = a.AnonymizeURI(overview.SignalState.URL)
|
||||||
|
overview.SignalState.Error = a.AnonymizeString(overview.SignalState.Error)
|
||||||
|
|
||||||
|
overview.IP = a.AnonymizeIPString(overview.IP)
|
||||||
|
for i, detail := range overview.Relays.Details {
|
||||||
|
detail.URI = a.AnonymizeURI(detail.URI)
|
||||||
|
detail.Error = a.AnonymizeString(detail.Error)
|
||||||
|
overview.Relays.Details[i] = detail
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, nsGroup := range overview.NSServerGroups {
|
||||||
|
for j, domain := range nsGroup.Domains {
|
||||||
|
overview.NSServerGroups[i].Domains[j] = a.AnonymizeDomain(domain)
|
||||||
|
}
|
||||||
|
for j, ns := range nsGroup.Servers {
|
||||||
|
host, port, err := net.SplitHostPort(ns)
|
||||||
|
if err == nil {
|
||||||
|
overview.NSServerGroups[i].Servers[j] = fmt.Sprintf("%s:%s", a.AnonymizeIPString(host), port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, route := range overview.Routes {
|
||||||
|
overview.Routes[i] = anonymizeRoute(a, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
overview.FQDN = a.AnonymizeDomain(overview.FQDN)
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeRoute(a *anonymize.Anonymizer, route string) string {
|
||||||
|
prefix, err := netip.ParsePrefix(route)
|
||||||
|
if err == nil {
|
||||||
|
ip := a.AnonymizeIPString(prefix.Addr().String())
|
||||||
|
return fmt.Sprintf("%s/%d", ip, prefix.Bits())
|
||||||
|
}
|
||||||
|
domains := strings.Split(route, ", ")
|
||||||
|
for i, domain := range domains {
|
||||||
|
domains[i] = a.AnonymizeDomain(domain)
|
||||||
|
}
|
||||||
|
return strings.Join(domains, ", ")
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -487,9 +489,15 @@ dnsServers:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParsingToDetail(t *testing.T) {
|
func TestParsingToDetail(t *testing.T) {
|
||||||
|
// Calculate time ago based on the fixture dates
|
||||||
|
lastConnectionUpdate1 := timeAgo(overview.Peers.Details[0].LastStatusUpdate)
|
||||||
|
lastHandshake1 := timeAgo(overview.Peers.Details[0].LastWireguardHandshake)
|
||||||
|
lastConnectionUpdate2 := timeAgo(overview.Peers.Details[1].LastStatusUpdate)
|
||||||
|
lastHandshake2 := timeAgo(overview.Peers.Details[1].LastWireguardHandshake)
|
||||||
|
|
||||||
detail := parseToFullDetailSummary(overview)
|
detail := parseToFullDetailSummary(overview)
|
||||||
|
|
||||||
expectedDetail :=
|
expectedDetail := fmt.Sprintf(
|
||||||
`Peers detail:
|
`Peers detail:
|
||||||
peer-1.awesome-domain.com:
|
peer-1.awesome-domain.com:
|
||||||
NetBird IP: 192.168.178.101
|
NetBird IP: 192.168.178.101
|
||||||
@@ -500,8 +508,8 @@ func TestParsingToDetail(t *testing.T) {
|
|||||||
Direct: true
|
Direct: true
|
||||||
ICE candidate (Local/Remote): -/-
|
ICE candidate (Local/Remote): -/-
|
||||||
ICE candidate endpoints (Local/Remote): -/-
|
ICE candidate endpoints (Local/Remote): -/-
|
||||||
Last connection update: 2001-01-01 01:01:01
|
Last connection update: %s
|
||||||
Last WireGuard handshake: 2001-01-01 01:01:02
|
Last WireGuard handshake: %s
|
||||||
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
|
||||||
@@ -516,15 +524,16 @@ func TestParsingToDetail(t *testing.T) {
|
|||||||
Direct: false
|
Direct: false
|
||||||
ICE candidate (Local/Remote): relay/prflx
|
ICE candidate (Local/Remote): relay/prflx
|
||||||
ICE candidate endpoints (Local/Remote): 10.0.0.1:10001/10.0.10.1:10002
|
ICE candidate endpoints (Local/Remote): 10.0.0.1:10001/10.0.10.1:10002
|
||||||
Last connection update: 2002-02-02 02:02:02
|
Last connection update: %s
|
||||||
Last WireGuard handshake: 2002-02-02 02:02:03
|
Last WireGuard handshake: %s
|
||||||
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: -
|
||||||
Latency: 10ms
|
Latency: 10ms
|
||||||
|
|
||||||
|
OS: %s/%s
|
||||||
Daemon version: 0.14.1
|
Daemon version: 0.14.1
|
||||||
CLI version: development
|
CLI version: %s
|
||||||
Management: Connected to my-awesome-management.com:443
|
Management: Connected to my-awesome-management.com:443
|
||||||
Signal: Connected to my-awesome-signal.com:443
|
Signal: Connected to my-awesome-signal.com:443
|
||||||
Relays:
|
Relays:
|
||||||
@@ -539,7 +548,7 @@ Interface type: Kernel
|
|||||||
Quantum resistance: false
|
Quantum resistance: false
|
||||||
Routes: 10.10.0.0/24
|
Routes: 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)
|
||||||
|
|
||||||
assert.Equal(t, expectedDetail, detail)
|
assert.Equal(t, expectedDetail, detail)
|
||||||
}
|
}
|
||||||
@@ -547,8 +556,8 @@ Peers count: 2/2 Connected
|
|||||||
func TestParsingToShortVersion(t *testing.T) {
|
func TestParsingToShortVersion(t *testing.T) {
|
||||||
shortVersion := parseGeneralSummary(overview, false, false, false)
|
shortVersion := parseGeneralSummary(overview, false, false, false)
|
||||||
|
|
||||||
expectedString :=
|
expectedString := fmt.Sprintf("OS: %s/%s", runtime.GOOS, runtime.GOARCH) + `
|
||||||
`Daemon version: 0.14.1
|
Daemon version: 0.14.1
|
||||||
CLI version: development
|
CLI version: development
|
||||||
Management: Connected
|
Management: Connected
|
||||||
Signal: Connected
|
Signal: Connected
|
||||||
@@ -572,3 +581,31 @@ func TestParsingOfIP(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, "192.168.178.123\n", parsedIP)
|
assert.Equal(t, "192.168.178.123\n", parsedIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTimeAgo(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
input time.Time
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"Now", now, "Now"},
|
||||||
|
{"Seconds ago", now.Add(-10 * time.Second), "10 seconds ago"},
|
||||||
|
{"One minute ago", now.Add(-1 * time.Minute), "1 minute ago"},
|
||||||
|
{"Minutes and seconds ago", now.Add(-(1*time.Minute + 30*time.Second)), "1 minute, 30 seconds ago"},
|
||||||
|
{"One hour ago", now.Add(-1 * time.Hour), "1 hour ago"},
|
||||||
|
{"Hours and minutes ago", now.Add(-(2*time.Hour + 15*time.Minute)), "2 hours, 15 minutes ago"},
|
||||||
|
{"One day ago", now.Add(-24 * time.Hour), "1 day ago"},
|
||||||
|
{"Multiple days ago", now.Add(-(72*time.Hour + 20*time.Minute)), "3 days ago"},
|
||||||
|
{"Zero time", time.Time{}, "-"},
|
||||||
|
{"Unix zero time", time.Unix(0, 0), "-"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := timeAgo(tc.input)
|
||||||
|
assert.Equal(t, tc.expected, result, "Failed %s", tc.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
@@ -14,6 +17,7 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
|
||||||
clientProto "github.com/netbirdio/netbird/client/proto"
|
clientProto "github.com/netbirdio/netbird/client/proto"
|
||||||
client "github.com/netbirdio/netbird/client/server"
|
client "github.com/netbirdio/netbird/client/server"
|
||||||
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
||||||
@@ -52,7 +56,10 @@ func startSignal(t *testing.T) (*grpc.Server, net.Listener) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
s := grpc.NewServer()
|
s := grpc.NewServer()
|
||||||
sigProto.RegisterSignalExchangeServer(s, sig.NewServer())
|
srv, err := sig.NewServer(otel.Meter(""))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sigProto.RegisterSignalExchangeServer(s, srv)
|
||||||
go func() {
|
go func() {
|
||||||
if err := s.Serve(lis); err != nil {
|
if err := s.Serve(lis); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -69,23 +76,24 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
s := grpc.NewServer()
|
s := grpc.NewServer()
|
||||||
store, err := mgmt.NewStoreFromJson(config.Datadir, nil)
|
store, cleanUp, err := mgmt.NewTestStoreFromJson(context.Background(), config.Datadir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
t.Cleanup(cleanUp)
|
||||||
|
|
||||||
peersUpdateManager := mgmt.NewPeersUpdateManager(nil)
|
peersUpdateManager := mgmt.NewPeersUpdateManager(nil)
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
iv, _ := integrations.NewIntegratedValidator(eventStore)
|
iv, _ := integrations.NewIntegratedValidator(context.Background(), eventStore)
|
||||||
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, iv)
|
accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, iv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
|
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, "")
|
||||||
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager, nil, nil)
|
mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, peersUpdateManager, turnManager, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -100,7 +108,7 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startClientDaemon(
|
func startClientDaemon(
|
||||||
t *testing.T, ctx context.Context, managementURL, configPath string,
|
t *testing.T, ctx context.Context, _, configPath string,
|
||||||
) (*grpc.Server, net.Listener) {
|
) (*grpc.Server, net.Listener) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
@@ -7,11 +7,13 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
gstatus "google.golang.org/grpc/status"
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
@@ -40,7 +42,12 @@ func init() {
|
|||||||
upCmd.PersistentFlags().BoolVarP(&foregroundMode, "foreground-mode", "F", false, "start service in foreground")
|
upCmd.PersistentFlags().BoolVarP(&foregroundMode, "foreground-mode", "F", false, "start service in foreground")
|
||||||
upCmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "Wireguard interface name")
|
upCmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "Wireguard interface name")
|
||||||
upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "Wireguard interface listening port")
|
upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "Wireguard interface listening port")
|
||||||
|
upCmd.PersistentFlags().BoolVarP(&networkMonitor, networkMonitorFlag, "N", networkMonitor,
|
||||||
|
`Manage network monitoring. Defaults to true on Windows and macOS, false on Linux. `+
|
||||||
|
`E.g. --network-monitor=false to disable or --network-monitor=true to enable.`,
|
||||||
|
)
|
||||||
upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening")
|
upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening")
|
||||||
|
upCmd.PersistentFlags().DurationVar(&dnsRouteInterval, dnsRouteIntervalFlag, time.Minute, "DNS route update interval")
|
||||||
}
|
}
|
||||||
|
|
||||||
func upFunc(cmd *cobra.Command, args []string) error {
|
func upFunc(cmd *cobra.Command, args []string) error {
|
||||||
@@ -116,6 +123,10 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
ic.WireguardPort = &p
|
ic.WireguardPort = &p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(networkMonitorFlag).Changed {
|
||||||
|
ic.NetworkMonitor = &networkMonitor
|
||||||
|
}
|
||||||
|
|
||||||
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
||||||
ic.PreSharedKey = &preSharedKey
|
ic.PreSharedKey = &preSharedKey
|
||||||
}
|
}
|
||||||
@@ -132,6 +143,10 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(dnsRouteIntervalFlag).Changed {
|
||||||
|
ic.DNSRouteInterval = &dnsRouteInterval
|
||||||
|
}
|
||||||
|
|
||||||
config, err := internal.UpdateOrCreateConfig(ic)
|
config, err := internal.UpdateOrCreateConfig(ic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get config file: %v", err)
|
return fmt.Errorf("get config file: %v", err)
|
||||||
@@ -147,7 +162,12 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
ctx, cancel = context.WithCancel(ctx)
|
ctx, cancel = context.WithCancel(ctx)
|
||||||
SetupCloseHandler(ctx, cancel)
|
SetupCloseHandler(ctx, cancel)
|
||||||
return internal.RunClient(ctx, config, peer.NewRecorder(config.ManagementURL.String()))
|
|
||||||
|
r := peer.NewRecorder(config.ManagementURL.String())
|
||||||
|
r.GetFullStatus()
|
||||||
|
|
||||||
|
connectClient := internal.NewConnectClient(ctx, config, r)
|
||||||
|
return connectClient.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
||||||
@@ -226,6 +246,14 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
loginRequest.WireguardPort = &wp
|
loginRequest.WireguardPort = &wp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(networkMonitorFlag).Changed {
|
||||||
|
loginRequest.NetworkMonitor = &networkMonitor
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(dnsRouteIntervalFlag).Changed {
|
||||||
|
loginRequest.DnsRouteInterval = durationpb.New(dnsRouteInterval)
|
||||||
|
}
|
||||||
|
|
||||||
var loginErr error
|
var loginErr error
|
||||||
|
|
||||||
var loginResp *proto.LoginResponse
|
var loginResp *proto.LoginResponse
|
||||||
|
|||||||
30
client/errors/errors.go
Normal file
30
client/errors/errors.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
)
|
||||||
|
|
||||||
|
func formatError(es []error) string {
|
||||||
|
if len(es) == 0 {
|
||||||
|
return fmt.Sprintf("0 error occurred:\n\t* %s", es[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
points := make([]string, len(es))
|
||||||
|
for i, err := range es {
|
||||||
|
points[i] = fmt.Sprintf("* %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%d errors occurred:\n\t%s",
|
||||||
|
len(es), strings.Join(points, "\n\t"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatErrorOrNil(err *multierror.Error) error {
|
||||||
|
if err != nil {
|
||||||
|
err.ErrorFormat = formatError
|
||||||
|
}
|
||||||
|
return err.ErrorOrNil()
|
||||||
|
}
|
||||||
@@ -42,20 +42,20 @@ func NewFirewall(context context.Context, iface IFaceMapper) (firewall.Manager,
|
|||||||
|
|
||||||
switch check() {
|
switch check() {
|
||||||
case IPTABLES:
|
case IPTABLES:
|
||||||
log.Debug("creating an iptables firewall manager")
|
log.Info("creating an iptables firewall manager")
|
||||||
fm, errFw = nbiptables.Create(context, iface)
|
fm, errFw = nbiptables.Create(context, iface)
|
||||||
if errFw != nil {
|
if errFw != nil {
|
||||||
log.Errorf("failed to create iptables manager: %s", errFw)
|
log.Errorf("failed to create iptables manager: %s", errFw)
|
||||||
}
|
}
|
||||||
case NFTABLES:
|
case NFTABLES:
|
||||||
log.Debug("creating an nftables firewall manager")
|
log.Info("creating an nftables firewall manager")
|
||||||
fm, errFw = nbnftables.Create(context, iface)
|
fm, errFw = nbnftables.Create(context, iface)
|
||||||
if errFw != nil {
|
if errFw != nil {
|
||||||
log.Errorf("failed to create nftables manager: %s", errFw)
|
log.Errorf("failed to create nftables manager: %s", errFw)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
errFw = fmt.Errorf("no firewall manager found")
|
errFw = fmt.Errorf("no firewall manager found")
|
||||||
log.Debug("no firewall manager found, try to use userspace packet filtering firewall")
|
log.Info("no firewall manager found, trying to use userspace packet filtering firewall")
|
||||||
}
|
}
|
||||||
|
|
||||||
if iface.IsUserspaceBind() {
|
if iface.IsUserspaceBind() {
|
||||||
@@ -85,16 +85,58 @@ func NewFirewall(context context.Context, iface IFaceMapper) (firewall.Manager,
|
|||||||
|
|
||||||
// check returns the firewall type based on common lib checks. It returns UNKNOWN if no firewall is found.
|
// check returns the firewall type based on common lib checks. It returns UNKNOWN if no firewall is found.
|
||||||
func check() FWType {
|
func check() FWType {
|
||||||
nf := nftables.Conn{}
|
useIPTABLES := false
|
||||||
if _, err := nf.ListChains(); err == nil && os.Getenv(SKIP_NFTABLES_ENV) != "true" {
|
var iptablesChains []string
|
||||||
return NFTABLES
|
ip, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
if err == nil && isIptablesClientAvailable(ip) {
|
||||||
|
major, minor, _ := ip.GetIptablesVersion()
|
||||||
|
// use iptables when its version is lower than 1.8.0 which doesn't work well with our nftables manager
|
||||||
|
if major < 1 || (major == 1 && minor < 8) {
|
||||||
|
return IPTABLES
|
||||||
|
}
|
||||||
|
|
||||||
|
useIPTABLES = true
|
||||||
|
|
||||||
|
iptablesChains, err = ip.ListChains("filter")
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to list iptables chains: %s", err)
|
||||||
|
useIPTABLES = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
nf := nftables.Conn{}
|
||||||
if err != nil {
|
if chains, err := nf.ListChains(); err == nil && os.Getenv(SKIP_NFTABLES_ENV) != "true" {
|
||||||
return UNKNOWN
|
if !useIPTABLES {
|
||||||
|
return NFTABLES
|
||||||
|
}
|
||||||
|
|
||||||
|
// search for chains where table is filter
|
||||||
|
// if we find one, we assume that nftables manager can be used with iptables
|
||||||
|
for _, chain := range chains {
|
||||||
|
if chain.Table.Name == "filter" {
|
||||||
|
return NFTABLES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check tables for the following constraints:
|
||||||
|
// 1. there is no chain in nftables for the filter table and there is at least one chain in iptables, we assume that nftables manager can not be used
|
||||||
|
// 2. there is no tables or more than one table, we assume that nftables manager can be used
|
||||||
|
// 3. there is only one table and its name is filter, we assume that nftables manager can not be used, since there was no chain in it
|
||||||
|
// 4. if we find an error we log and continue with iptables check
|
||||||
|
nbTablesList, err := nf.ListTables()
|
||||||
|
switch {
|
||||||
|
case err == nil && len(iptablesChains) > 0:
|
||||||
|
return IPTABLES
|
||||||
|
case err == nil && len(nbTablesList) != 1:
|
||||||
|
return NFTABLES
|
||||||
|
case err == nil && len(nbTablesList) == 1 && nbTablesList[0].Name == "filter":
|
||||||
|
return IPTABLES
|
||||||
|
case err != nil:
|
||||||
|
log.Errorf("failed to list nftables tables on fw manager discovery: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if isIptablesClientAvailable(ip) {
|
|
||||||
|
if useIPTABLES {
|
||||||
return IPTABLES
|
return IPTABLES
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,12 +74,12 @@ func (i *routerManager) InsertRoutingRules(pair firewall.RouterPair) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = i.insertRoutingRule(firewall.NatFormat, tableNat, chainRTNAT, routingFinalNatJump, pair)
|
err = i.addNATRule(firewall.NatFormat, tableNat, chainRTNAT, routingFinalNatJump, pair)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = i.insertRoutingRule(firewall.InNatFormat, tableNat, chainRTNAT, routingFinalNatJump, firewall.GetInPair(pair))
|
err = i.addNATRule(firewall.InNatFormat, tableNat, chainRTNAT, routingFinalNatJump, firewall.GetInPair(pair))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -87,12 +87,12 @@ func (i *routerManager) InsertRoutingRules(pair firewall.RouterPair) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertRoutingRule inserts an iptable rule
|
// insertRoutingRule inserts an iptables rule
|
||||||
func (i *routerManager) insertRoutingRule(keyFormat, table, chain, jump string, pair firewall.RouterPair) error {
|
func (i *routerManager) insertRoutingRule(keyFormat, table, chain, jump string, pair firewall.RouterPair) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
ruleKey := firewall.GenKey(keyFormat, pair.ID)
|
ruleKey := firewall.GenKey(keyFormat, pair.ID)
|
||||||
rule := genRuleSpec(jump, ruleKey, pair.Source, pair.Destination)
|
rule := genRuleSpec(jump, pair.Source, pair.Destination)
|
||||||
existingRule, found := i.rules[ruleKey]
|
existingRule, found := i.rules[ruleKey]
|
||||||
if found {
|
if found {
|
||||||
err = i.iptablesClient.DeleteIfExists(table, chain, existingRule...)
|
err = i.iptablesClient.DeleteIfExists(table, chain, existingRule...)
|
||||||
@@ -101,6 +101,7 @@ func (i *routerManager) insertRoutingRule(keyFormat, table, chain, jump string,
|
|||||||
}
|
}
|
||||||
delete(i.rules, ruleKey)
|
delete(i.rules, ruleKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = i.iptablesClient.Insert(table, chain, 1, rule...)
|
err = i.iptablesClient.Insert(table, chain, 1, rule...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while adding new %s rule for %s: %v", getIptablesRuleType(table), pair.Destination, err)
|
return fmt.Errorf("error while adding new %s rule for %s: %v", getIptablesRuleType(table), pair.Destination, err)
|
||||||
@@ -317,6 +318,13 @@ func (i *routerManager) createChain(table, newChain string) error {
|
|||||||
return fmt.Errorf("couldn't create chain %s in %s table, error: %v", newChain, table, err)
|
return fmt.Errorf("couldn't create chain %s in %s table, error: %v", newChain, table, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the loopback return rule to the NAT chain
|
||||||
|
loopbackRule := []string{"-o", "lo", "-j", "RETURN"}
|
||||||
|
err = i.iptablesClient.Insert(table, newChain, 1, loopbackRule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add loopback return rule to %s: %v", chainRTNAT, err)
|
||||||
|
}
|
||||||
|
|
||||||
err = i.iptablesClient.Append(table, newChain, "-j", "RETURN")
|
err = i.iptablesClient.Append(table, newChain, "-j", "RETURN")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("couldn't create chain %s default rule, error: %v", newChain, err)
|
return fmt.Errorf("couldn't create chain %s default rule, error: %v", newChain, err)
|
||||||
@@ -326,9 +334,33 @@ func (i *routerManager) createChain(table, newChain string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// genRuleSpec generates rule specification with comment identifier
|
// addNATRule appends an iptables rule pair to the nat chain
|
||||||
func genRuleSpec(jump, id, source, destination string) []string {
|
func (i *routerManager) addNATRule(keyFormat, table, chain, jump string, pair firewall.RouterPair) error {
|
||||||
return []string{"-s", source, "-d", destination, "-j", jump, "-m", "comment", "--comment", id}
|
ruleKey := firewall.GenKey(keyFormat, pair.ID)
|
||||||
|
rule := genRuleSpec(jump, pair.Source, pair.Destination)
|
||||||
|
existingRule, found := i.rules[ruleKey]
|
||||||
|
if found {
|
||||||
|
err := i.iptablesClient.DeleteIfExists(table, chain, existingRule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while removing existing NAT rule for %s: %v", pair.Destination, err)
|
||||||
|
}
|
||||||
|
delete(i.rules, ruleKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inserting after loopback ignore rule
|
||||||
|
err := i.iptablesClient.Insert(table, chain, 2, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while appending new NAT rule for %s: %v", pair.Destination, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.rules[ruleKey] = rule
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// genRuleSpec generates rule specification
|
||||||
|
func genRuleSpec(jump, source, destination string) []string {
|
||||||
|
return []string{"-s", source, "-d", destination, "-j", jump}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIptablesRuleType(table string) string {
|
func getIptablesRuleType(table string) string {
|
||||||
|
|||||||
@@ -51,14 +51,12 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
|
|||||||
Destination: "100.100.100.0/24",
|
Destination: "100.100.100.0/24",
|
||||||
Masquerade: true,
|
Masquerade: true,
|
||||||
}
|
}
|
||||||
forward4RuleKey := firewall.GenKey(firewall.ForwardingFormat, pair.ID)
|
forward4Rule := genRuleSpec(routingFinalForwardJump, pair.Source, pair.Destination)
|
||||||
forward4Rule := genRuleSpec(routingFinalForwardJump, forward4RuleKey, pair.Source, pair.Destination)
|
|
||||||
|
|
||||||
err = manager.iptablesClient.Insert(tableFilter, chainRTFWD, 1, forward4Rule...)
|
err = manager.iptablesClient.Insert(tableFilter, chainRTFWD, 1, forward4Rule...)
|
||||||
require.NoError(t, err, "inserting rule should not return error")
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
nat4RuleKey := firewall.GenKey(firewall.NatFormat, pair.ID)
|
nat4Rule := genRuleSpec(routingFinalNatJump, pair.Source, pair.Destination)
|
||||||
nat4Rule := genRuleSpec(routingFinalNatJump, nat4RuleKey, pair.Source, pair.Destination)
|
|
||||||
|
|
||||||
err = manager.iptablesClient.Insert(tableNat, chainRTNAT, 1, nat4Rule...)
|
err = manager.iptablesClient.Insert(tableNat, chainRTNAT, 1, nat4Rule...)
|
||||||
require.NoError(t, err, "inserting rule should not return error")
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
@@ -92,7 +90,7 @@ func TestIptablesManager_InsertRoutingRules(t *testing.T) {
|
|||||||
require.NoError(t, err, "forwarding pair should be inserted")
|
require.NoError(t, err, "forwarding pair should be inserted")
|
||||||
|
|
||||||
forwardRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID)
|
forwardRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID)
|
||||||
forwardRule := genRuleSpec(routingFinalForwardJump, forwardRuleKey, testCase.InputPair.Source, testCase.InputPair.Destination)
|
forwardRule := genRuleSpec(routingFinalForwardJump, testCase.InputPair.Source, testCase.InputPair.Destination)
|
||||||
|
|
||||||
exists, err := iptablesClient.Exists(tableFilter, chainRTFWD, forwardRule...)
|
exists, err := iptablesClient.Exists(tableFilter, chainRTFWD, forwardRule...)
|
||||||
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableFilter, chainRTFWD)
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableFilter, chainRTFWD)
|
||||||
@@ -103,7 +101,7 @@ func TestIptablesManager_InsertRoutingRules(t *testing.T) {
|
|||||||
require.Equal(t, forwardRule[:4], foundRule[:4], "stored forwarding rule should match")
|
require.Equal(t, forwardRule[:4], foundRule[:4], "stored forwarding rule should match")
|
||||||
|
|
||||||
inForwardRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID)
|
inForwardRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID)
|
||||||
inForwardRule := genRuleSpec(routingFinalForwardJump, inForwardRuleKey, firewall.GetInPair(testCase.InputPair).Source, firewall.GetInPair(testCase.InputPair).Destination)
|
inForwardRule := genRuleSpec(routingFinalForwardJump, firewall.GetInPair(testCase.InputPair).Source, firewall.GetInPair(testCase.InputPair).Destination)
|
||||||
|
|
||||||
exists, err = iptablesClient.Exists(tableFilter, chainRTFWD, inForwardRule...)
|
exists, err = iptablesClient.Exists(tableFilter, chainRTFWD, inForwardRule...)
|
||||||
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableFilter, chainRTFWD)
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableFilter, chainRTFWD)
|
||||||
@@ -114,7 +112,7 @@ func TestIptablesManager_InsertRoutingRules(t *testing.T) {
|
|||||||
require.Equal(t, inForwardRule[:4], foundRule[:4], "stored income forwarding rule should match")
|
require.Equal(t, inForwardRule[:4], foundRule[:4], "stored income forwarding rule should match")
|
||||||
|
|
||||||
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID)
|
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID)
|
||||||
natRule := genRuleSpec(routingFinalNatJump, natRuleKey, testCase.InputPair.Source, testCase.InputPair.Destination)
|
natRule := genRuleSpec(routingFinalNatJump, testCase.InputPair.Source, testCase.InputPair.Destination)
|
||||||
|
|
||||||
exists, err = iptablesClient.Exists(tableNat, chainRTNAT, natRule...)
|
exists, err = iptablesClient.Exists(tableNat, chainRTNAT, natRule...)
|
||||||
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainRTNAT)
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainRTNAT)
|
||||||
@@ -130,7 +128,7 @@ func TestIptablesManager_InsertRoutingRules(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID)
|
inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID)
|
||||||
inNatRule := genRuleSpec(routingFinalNatJump, inNatRuleKey, firewall.GetInPair(testCase.InputPair).Source, firewall.GetInPair(testCase.InputPair).Destination)
|
inNatRule := genRuleSpec(routingFinalNatJump, firewall.GetInPair(testCase.InputPair).Source, firewall.GetInPair(testCase.InputPair).Destination)
|
||||||
|
|
||||||
exists, err = iptablesClient.Exists(tableNat, chainRTNAT, inNatRule...)
|
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)
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainRTNAT)
|
||||||
@@ -167,25 +165,25 @@ func TestIptablesManager_RemoveRoutingRules(t *testing.T) {
|
|||||||
require.NoError(t, err, "shouldn't return error")
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
forwardRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID)
|
forwardRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID)
|
||||||
forwardRule := genRuleSpec(routingFinalForwardJump, forwardRuleKey, testCase.InputPair.Source, testCase.InputPair.Destination)
|
forwardRule := genRuleSpec(routingFinalForwardJump, testCase.InputPair.Source, testCase.InputPair.Destination)
|
||||||
|
|
||||||
err = iptablesClient.Insert(tableFilter, chainRTFWD, 1, forwardRule...)
|
err = iptablesClient.Insert(tableFilter, chainRTFWD, 1, forwardRule...)
|
||||||
require.NoError(t, err, "inserting rule should not return error")
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
inForwardRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID)
|
inForwardRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID)
|
||||||
inForwardRule := genRuleSpec(routingFinalForwardJump, inForwardRuleKey, firewall.GetInPair(testCase.InputPair).Source, firewall.GetInPair(testCase.InputPair).Destination)
|
inForwardRule := genRuleSpec(routingFinalForwardJump, firewall.GetInPair(testCase.InputPair).Source, firewall.GetInPair(testCase.InputPair).Destination)
|
||||||
|
|
||||||
err = iptablesClient.Insert(tableFilter, chainRTFWD, 1, inForwardRule...)
|
err = iptablesClient.Insert(tableFilter, chainRTFWD, 1, inForwardRule...)
|
||||||
require.NoError(t, err, "inserting rule should not return error")
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID)
|
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID)
|
||||||
natRule := genRuleSpec(routingFinalNatJump, natRuleKey, testCase.InputPair.Source, testCase.InputPair.Destination)
|
natRule := genRuleSpec(routingFinalNatJump, testCase.InputPair.Source, testCase.InputPair.Destination)
|
||||||
|
|
||||||
err = iptablesClient.Insert(tableNat, chainRTNAT, 1, natRule...)
|
err = iptablesClient.Insert(tableNat, chainRTNAT, 1, natRule...)
|
||||||
require.NoError(t, err, "inserting rule should not return error")
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID)
|
inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID)
|
||||||
inNatRule := genRuleSpec(routingFinalNatJump, inNatRuleKey, firewall.GetInPair(testCase.InputPair).Source, firewall.GetInPair(testCase.InputPair).Destination)
|
inNatRule := genRuleSpec(routingFinalNatJump, firewall.GetInPair(testCase.InputPair).Source, firewall.GetInPair(testCase.InputPair).Destination)
|
||||||
|
|
||||||
err = iptablesClient.Insert(tableNat, chainRTNAT, 1, inNatRule...)
|
err = iptablesClient.Insert(tableNat, chainRTNAT, 1, inNatRule...)
|
||||||
require.NoError(t, err, "inserting rule should not return error")
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func (m *Manager) InsertRoutingRules(pair firewall.RouterPair) error {
|
|||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
return m.router.InsertRoutingRules(pair)
|
return m.router.AddRoutingRules(pair)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) RemoveRoutingRules(pair firewall.RouterPair) error {
|
func (m *Manager) RemoveRoutingRules(pair firewall.RouterPair) error {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const (
|
|||||||
|
|
||||||
userDataAcceptForwardRuleSrc = "frwacceptsrc"
|
userDataAcceptForwardRuleSrc = "frwacceptsrc"
|
||||||
userDataAcceptForwardRuleDst = "frwacceptdst"
|
userDataAcceptForwardRuleDst = "frwacceptdst"
|
||||||
|
|
||||||
|
loopbackInterface = "lo\x00"
|
||||||
)
|
)
|
||||||
|
|
||||||
// some presets for building nftable rules
|
// some presets for building nftable rules
|
||||||
@@ -126,6 +128,22 @@ func (r *router) createContainers() error {
|
|||||||
Type: nftables.ChainTypeNAT,
|
Type: nftables.ChainTypeNAT,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add RETURN rule for loopback interface
|
||||||
|
loRule := &nftables.Rule{
|
||||||
|
Table: r.workTable,
|
||||||
|
Chain: r.chains[chainNameRoutingNat],
|
||||||
|
Exprs: []expr.Any{
|
||||||
|
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 1,
|
||||||
|
Data: []byte(loopbackInterface),
|
||||||
|
},
|
||||||
|
&expr.Verdict{Kind: expr.VerdictReturn},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r.conn.InsertRule(loRule)
|
||||||
|
|
||||||
err := r.refreshRulesMap()
|
err := r.refreshRulesMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
||||||
@@ -138,28 +156,28 @@ func (r *router) createContainers() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InsertRoutingRules inserts a nftable rule pair to the forwarding chain and if enabled, to the nat chain
|
// AddRoutingRules appends a nftable rule pair to the forwarding chain and if enabled, to the nat chain
|
||||||
func (r *router) InsertRoutingRules(pair manager.RouterPair) error {
|
func (r *router) AddRoutingRules(pair manager.RouterPair) error {
|
||||||
err := r.refreshRulesMap()
|
err := r.refreshRulesMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.insertRoutingRule(manager.ForwardingFormat, chainNameRouteingFw, pair, false)
|
err = r.addRoutingRule(manager.ForwardingFormat, chainNameRouteingFw, pair, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = r.insertRoutingRule(manager.InForwardingFormat, chainNameRouteingFw, manager.GetInPair(pair), false)
|
err = r.addRoutingRule(manager.InForwardingFormat, chainNameRouteingFw, manager.GetInPair(pair), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if pair.Masquerade {
|
if pair.Masquerade {
|
||||||
err = r.insertRoutingRule(manager.NatFormat, chainNameRoutingNat, pair, true)
|
err = r.addRoutingRule(manager.NatFormat, chainNameRoutingNat, pair, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = r.insertRoutingRule(manager.InNatFormat, chainNameRoutingNat, manager.GetInPair(pair), true)
|
err = r.addRoutingRule(manager.InNatFormat, chainNameRoutingNat, manager.GetInPair(pair), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -177,8 +195,8 @@ func (r *router) InsertRoutingRules(pair manager.RouterPair) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertRoutingRule inserts a nftable rule to the conn client flush queue
|
// addRoutingRule inserts a nftable rule to the conn client flush queue
|
||||||
func (r *router) insertRoutingRule(format, chainName string, pair manager.RouterPair, isNat bool) error {
|
func (r *router) addRoutingRule(format, chainName string, pair manager.RouterPair, isNat bool) error {
|
||||||
sourceExp := generateCIDRMatcherExpressions(true, pair.Source)
|
sourceExp := generateCIDRMatcherExpressions(true, pair.Source)
|
||||||
destExp := generateCIDRMatcherExpressions(false, pair.Destination)
|
destExp := generateCIDRMatcherExpressions(false, pair.Destination)
|
||||||
|
|
||||||
@@ -199,7 +217,7 @@ func (r *router) insertRoutingRule(format, chainName string, pair manager.Router
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.rules[ruleKey] = r.conn.InsertRule(&nftables.Rule{
|
r.rules[ruleKey] = r.conn.AddRule(&nftables.Rule{
|
||||||
Table: r.workTable,
|
Table: r.workTable,
|
||||||
Chain: r.chains[chainName],
|
Chain: r.chains[chainName],
|
||||||
Exprs: expression,
|
Exprs: expression,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, err, "shouldn't return error")
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
err = manager.InsertRoutingRules(testCase.InputPair)
|
err = manager.AddRoutingRules(testCase.InputPair)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = manager.RemoveRoutingRules(testCase.InputPair)
|
_ = manager.RemoveRoutingRules(testCase.InputPair)
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -64,15 +64,18 @@ func manageFirewallRule(ruleName string, action action, extraArgs ...string) err
|
|||||||
if action == addRule {
|
if action == addRule {
|
||||||
args = append(args, extraArgs...)
|
args = append(args, extraArgs...)
|
||||||
}
|
}
|
||||||
|
netshCmd := GetSystem32Command("netsh")
|
||||||
cmd := exec.Command("netsh", args...)
|
cmd := exec.Command(netshCmd, args...)
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func isWindowsFirewallReachable() bool {
|
func isWindowsFirewallReachable() bool {
|
||||||
args := []string{"advfirewall", "show", "allprofiles", "state"}
|
args := []string{"advfirewall", "show", "allprofiles", "state"}
|
||||||
cmd := exec.Command("netsh", args...)
|
|
||||||
|
netshCmd := GetSystem32Command("netsh")
|
||||||
|
|
||||||
|
cmd := exec.Command(netshCmd, args...)
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||||
|
|
||||||
_, err := cmd.Output()
|
_, err := cmd.Output()
|
||||||
@@ -87,8 +90,23 @@ func isWindowsFirewallReachable() bool {
|
|||||||
func isFirewallRuleActive(ruleName string) bool {
|
func isFirewallRuleActive(ruleName string) bool {
|
||||||
args := []string{"advfirewall", "firewall", "show", "rule", "name=" + ruleName}
|
args := []string{"advfirewall", "firewall", "show", "rule", "name=" + ruleName}
|
||||||
|
|
||||||
cmd := exec.Command("netsh", args...)
|
netshCmd := GetSystem32Command("netsh")
|
||||||
|
|
||||||
|
cmd := exec.Command(netshCmd, args...)
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||||
_, err := cmd.Output()
|
_, err := cmd.Output()
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSystem32Command checks if a command can be found in the system path and returns it. In case it can't find it
|
||||||
|
// in the path it will return the full path of a command assuming C:\windows\system32 as the base path.
|
||||||
|
func GetSystem32Command(command string) string {
|
||||||
|
_, err := exec.LookPath(command)
|
||||||
|
if err == nil {
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Command %s not found in PATH, using C:\\windows\\system32\\%s.exe path", command, command)
|
||||||
|
|
||||||
|
return "C:\\windows\\system32\\" + command + ".exe"
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,12 +5,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
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/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
|
||||||
"github.com/netbirdio/netbird/client/ssh"
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
mgm "github.com/netbirdio/netbird/management/client"
|
mgm "github.com/netbirdio/netbird/management/client"
|
||||||
@@ -48,8 +53,10 @@ type ConfigInput struct {
|
|||||||
RosenpassPermissive *bool
|
RosenpassPermissive *bool
|
||||||
InterfaceName *string
|
InterfaceName *string
|
||||||
WireguardPort *int
|
WireguardPort *int
|
||||||
|
NetworkMonitor *bool
|
||||||
DisableAutoConnect *bool
|
DisableAutoConnect *bool
|
||||||
ExtraIFaceBlackList []string
|
ExtraIFaceBlackList []string
|
||||||
|
DNSRouteInterval *time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config Configuration type
|
// Config Configuration type
|
||||||
@@ -61,6 +68,7 @@ type Config struct {
|
|||||||
AdminURL *url.URL
|
AdminURL *url.URL
|
||||||
WgIface string
|
WgIface string
|
||||||
WgPort int
|
WgPort int
|
||||||
|
NetworkMonitor *bool
|
||||||
IFaceBlackList []string
|
IFaceBlackList []string
|
||||||
DisableIPv6Discovery bool
|
DisableIPv6Discovery bool
|
||||||
RosenpassEnabled bool
|
RosenpassEnabled bool
|
||||||
@@ -91,6 +99,9 @@ type Config struct {
|
|||||||
// DisableAutoConnect determines whether the client should not start with the service
|
// DisableAutoConnect determines whether the client should not start with the service
|
||||||
// it's set to false by default due to backwards compatibility
|
// it's set to false by default due to backwards compatibility
|
||||||
DisableAutoConnect bool
|
DisableAutoConnect bool
|
||||||
|
|
||||||
|
// DNSRouteInterval is the interval in which the DNS routes are updated
|
||||||
|
DNSRouteInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -100,6 +111,14 @@ func ReadConfig(configPath string) (*Config, error) {
|
|||||||
if _, err := util.ReadJson(configPath, config); err != nil {
|
if _, err := util.ReadJson(configPath, config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// initialize through apply() without changes
|
||||||
|
if changed, err := config.apply(ConfigInput{}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if changed {
|
||||||
|
if err = WriteOutConfig(configPath, config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
@@ -152,79 +171,15 @@ func WriteOutConfig(path string, config *Config) error {
|
|||||||
|
|
||||||
// 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
|
||||||
func createNewConfig(input ConfigInput) (*Config, error) {
|
func createNewConfig(input ConfigInput) (*Config, error) {
|
||||||
wgKey := generateKey()
|
|
||||||
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
config := &Config{
|
config := &Config{
|
||||||
SSHKey: string(pem),
|
// defaults to false only for new (post 0.26) configurations
|
||||||
PrivateKey: wgKey,
|
ServerSSHAllowed: util.False(),
|
||||||
IFaceBlackList: []string{},
|
|
||||||
DisableIPv6Discovery: false,
|
|
||||||
NATExternalIPs: input.NATExternalIPs,
|
|
||||||
CustomDNSAddress: string(input.CustomDNSAddress),
|
|
||||||
ServerSSHAllowed: util.False(),
|
|
||||||
DisableAutoConnect: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
|
if _, err := config.apply(input); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config.ManagementURL = defaultManagementURL
|
|
||||||
if input.ManagementURL != "" {
|
|
||||||
URL, err := parseURL("Management URL", input.ManagementURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.ManagementURL = URL
|
|
||||||
}
|
|
||||||
|
|
||||||
config.WgPort = iface.DefaultWgPort
|
|
||||||
if input.WireguardPort != nil {
|
|
||||||
config.WgPort = *input.WireguardPort
|
|
||||||
}
|
|
||||||
|
|
||||||
config.WgIface = iface.WgInterfaceDefault
|
|
||||||
if input.InterfaceName != nil {
|
|
||||||
config.WgIface = *input.InterfaceName
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.PreSharedKey != nil {
|
|
||||||
config.PreSharedKey = *input.PreSharedKey
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.RosenpassEnabled != nil {
|
|
||||||
config.RosenpassEnabled = *input.RosenpassEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.RosenpassPermissive != nil {
|
|
||||||
config.RosenpassPermissive = *input.RosenpassPermissive
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.ServerSSHAllowed != nil {
|
|
||||||
config.ServerSSHAllowed = input.ServerSSHAllowed
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultAdminURL, err := parseURL("Admin URL", DefaultAdminURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
config.AdminURL = defaultAdminURL
|
|
||||||
if input.AdminURL != "" {
|
|
||||||
newURL, err := parseURL("Admin Panel URL", input.AdminURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.AdminURL = newURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// nolint:gocritic
|
|
||||||
config.IFaceBlackList = append(defaultInterfaceBlacklist, input.ExtraIFaceBlackList...)
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,104 +190,12 @@ func update(input ConfigInput) (*Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh := false
|
updated, err := config.apply(input)
|
||||||
|
if err != nil {
|
||||||
if input.ManagementURL != "" && config.ManagementURL.String() != input.ManagementURL {
|
return nil, err
|
||||||
log.Infof("new Management URL provided, updated to %s (old value %s)",
|
|
||||||
input.ManagementURL, config.ManagementURL)
|
|
||||||
newURL, err := parseURL("Management URL", input.ManagementURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.ManagementURL = newURL
|
|
||||||
refresh = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.AdminURL != "" && (config.AdminURL == nil || config.AdminURL.String() != input.AdminURL) {
|
if updated {
|
||||||
log.Infof("new Admin Panel URL provided, updated to %s (old value %s)",
|
|
||||||
input.AdminURL, config.AdminURL)
|
|
||||||
newURL, err := parseURL("Admin Panel URL", input.AdminURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.AdminURL = newURL
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.PreSharedKey != nil && config.PreSharedKey != *input.PreSharedKey {
|
|
||||||
log.Infof("new pre-shared key provided, replacing old key")
|
|
||||||
config.PreSharedKey = *input.PreSharedKey
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.SSHKey == "" {
|
|
||||||
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.SSHKey = string(pem)
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.WgPort == 0 {
|
|
||||||
config.WgPort = iface.DefaultWgPort
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.WireguardPort != nil {
|
|
||||||
config.WgPort = *input.WireguardPort
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.InterfaceName != nil {
|
|
||||||
config.WgIface = *input.InterfaceName
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.NATExternalIPs != nil && len(config.NATExternalIPs) != len(input.NATExternalIPs) {
|
|
||||||
config.NATExternalIPs = input.NATExternalIPs
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.CustomDNSAddress != nil {
|
|
||||||
config.CustomDNSAddress = string(input.CustomDNSAddress)
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.RosenpassEnabled != nil {
|
|
||||||
config.RosenpassEnabled = *input.RosenpassEnabled
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.RosenpassPermissive != nil {
|
|
||||||
config.RosenpassPermissive = *input.RosenpassPermissive
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.DisableAutoConnect != nil {
|
|
||||||
config.DisableAutoConnect = *input.DisableAutoConnect
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.ServerSSHAllowed != nil {
|
|
||||||
config.ServerSSHAllowed = input.ServerSSHAllowed
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.ServerSSHAllowed == nil {
|
|
||||||
config.ServerSSHAllowed = util.True()
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(input.ExtraIFaceBlackList) > 0 {
|
|
||||||
for _, iFace := range util.SliceDiff(input.ExtraIFaceBlackList, config.IFaceBlackList) {
|
|
||||||
config.IFaceBlackList = append(config.IFaceBlackList, iFace)
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if refresh {
|
|
||||||
// since we have new management URL, we need to update config file
|
|
||||||
if err := util.WriteJson(input.ConfigPath, config); err != nil {
|
if err := util.WriteJson(input.ConfigPath, config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -341,6 +204,190 @@ func update(input ConfigInput) (*Config, error) {
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (config *Config) apply(input ConfigInput) (updated bool, err error) {
|
||||||
|
if config.ManagementURL == nil {
|
||||||
|
log.Infof("using default Management URL %s", DefaultManagementURL)
|
||||||
|
config.ManagementURL, err = parseURL("Management URL", DefaultManagementURL)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if input.ManagementURL != "" && input.ManagementURL != config.ManagementURL.String() {
|
||||||
|
log.Infof("new Management URL provided, updated to %#v (old value %#v)",
|
||||||
|
input.ManagementURL, config.ManagementURL.String())
|
||||||
|
URL, err := parseURL("Management URL", input.ManagementURL)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
config.ManagementURL = URL
|
||||||
|
updated = true
|
||||||
|
} else if config.ManagementURL == nil {
|
||||||
|
log.Infof("using default Management URL %s", DefaultManagementURL)
|
||||||
|
config.ManagementURL, err = parseURL("Management URL", DefaultManagementURL)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.AdminURL == nil {
|
||||||
|
log.Infof("using default Admin URL %s", DefaultManagementURL)
|
||||||
|
config.AdminURL, err = parseURL("Admin URL", DefaultAdminURL)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if input.AdminURL != "" && input.AdminURL != config.AdminURL.String() {
|
||||||
|
log.Infof("new Admin Panel URL provided, updated to %#v (old value %#v)",
|
||||||
|
input.AdminURL, config.AdminURL.String())
|
||||||
|
newURL, err := parseURL("Admin Panel URL", input.AdminURL)
|
||||||
|
if err != nil {
|
||||||
|
return updated, err
|
||||||
|
}
|
||||||
|
config.AdminURL = newURL
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.PrivateKey == "" {
|
||||||
|
log.Infof("generated new Wireguard key")
|
||||||
|
config.PrivateKey = generateKey()
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.SSHKey == "" {
|
||||||
|
log.Infof("generated new SSH key")
|
||||||
|
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
config.SSHKey = string(pem)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.WireguardPort != nil && *input.WireguardPort != config.WgPort {
|
||||||
|
log.Infof("updating Wireguard port %d (old value %d)",
|
||||||
|
*input.WireguardPort, config.WgPort)
|
||||||
|
config.WgPort = *input.WireguardPort
|
||||||
|
updated = true
|
||||||
|
} else if config.WgPort == 0 {
|
||||||
|
config.WgPort = iface.DefaultWgPort
|
||||||
|
log.Infof("using default Wireguard port %d", config.WgPort)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.InterfaceName != nil && *input.InterfaceName != config.WgIface {
|
||||||
|
log.Infof("updating Wireguard interface %#v (old value %#v)",
|
||||||
|
*input.InterfaceName, config.WgIface)
|
||||||
|
config.WgIface = *input.InterfaceName
|
||||||
|
updated = true
|
||||||
|
} else if config.WgIface == "" {
|
||||||
|
config.WgIface = iface.WgInterfaceDefault
|
||||||
|
log.Infof("using default Wireguard interface %s", config.WgIface)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.NATExternalIPs != nil && !reflect.DeepEqual(config.NATExternalIPs, input.NATExternalIPs) {
|
||||||
|
log.Infof("updating NAT External IP [ %s ] (old value: [ %s ])",
|
||||||
|
strings.Join(input.NATExternalIPs, " "),
|
||||||
|
strings.Join(config.NATExternalIPs, " "))
|
||||||
|
config.NATExternalIPs = input.NATExternalIPs
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.PreSharedKey != nil && *input.PreSharedKey != config.PreSharedKey {
|
||||||
|
log.Infof("new pre-shared key provided, replacing old key")
|
||||||
|
config.PreSharedKey = *input.PreSharedKey
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.RosenpassEnabled != nil && *input.RosenpassEnabled != config.RosenpassEnabled {
|
||||||
|
log.Infof("switching Rosenpass to %t", *input.RosenpassEnabled)
|
||||||
|
config.RosenpassEnabled = *input.RosenpassEnabled
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.RosenpassPermissive != nil && *input.RosenpassPermissive != config.RosenpassPermissive {
|
||||||
|
log.Infof("switching Rosenpass permissive to %t", *input.RosenpassPermissive)
|
||||||
|
config.RosenpassPermissive = *input.RosenpassPermissive
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.NetworkMonitor != nil && input.NetworkMonitor != config.NetworkMonitor {
|
||||||
|
log.Infof("switching Network Monitor to %t", *input.NetworkMonitor)
|
||||||
|
config.NetworkMonitor = input.NetworkMonitor
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.NetworkMonitor == nil {
|
||||||
|
// enable network monitoring by default on windows and darwin clients
|
||||||
|
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
|
||||||
|
enabled := true
|
||||||
|
config.NetworkMonitor = &enabled
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.CustomDNSAddress != nil && string(input.CustomDNSAddress) != config.CustomDNSAddress {
|
||||||
|
log.Infof("updating custom DNS address %#v (old value %#v)",
|
||||||
|
string(input.CustomDNSAddress), config.CustomDNSAddress)
|
||||||
|
config.CustomDNSAddress = string(input.CustomDNSAddress)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.IFaceBlackList) == 0 {
|
||||||
|
log.Infof("filling in interface blacklist with defaults: [ %s ]",
|
||||||
|
strings.Join(defaultInterfaceBlacklist, " "))
|
||||||
|
config.IFaceBlackList = append(config.IFaceBlackList, defaultInterfaceBlacklist...)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(input.ExtraIFaceBlackList) > 0 {
|
||||||
|
for _, iFace := range util.SliceDiff(input.ExtraIFaceBlackList, config.IFaceBlackList) {
|
||||||
|
log.Infof("adding new entry to interface blacklist: %s", iFace)
|
||||||
|
config.IFaceBlackList = append(config.IFaceBlackList, iFace)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.DisableAutoConnect != nil && *input.DisableAutoConnect != config.DisableAutoConnect {
|
||||||
|
if *input.DisableAutoConnect {
|
||||||
|
log.Infof("turning off automatic connection on startup")
|
||||||
|
} else {
|
||||||
|
log.Infof("enabling automatic connection on startup")
|
||||||
|
}
|
||||||
|
config.DisableAutoConnect = *input.DisableAutoConnect
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.ServerSSHAllowed != nil && *input.ServerSSHAllowed != *config.ServerSSHAllowed {
|
||||||
|
if *input.ServerSSHAllowed {
|
||||||
|
log.Infof("enabling SSH server")
|
||||||
|
} else {
|
||||||
|
log.Infof("disabling SSH server")
|
||||||
|
}
|
||||||
|
config.ServerSSHAllowed = input.ServerSSHAllowed
|
||||||
|
updated = true
|
||||||
|
} else if config.ServerSSHAllowed == nil {
|
||||||
|
// enables SSH for configs from old versions to preserve backwards compatibility
|
||||||
|
log.Infof("falling back to enabled SSH server for pre-existing configuration")
|
||||||
|
config.ServerSSHAllowed = util.True()
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.DNSRouteInterval != nil && *input.DNSRouteInterval != config.DNSRouteInterval {
|
||||||
|
log.Infof("updating DNS route interval to %s (old value %s)",
|
||||||
|
input.DNSRouteInterval.String(), config.DNSRouteInterval.String())
|
||||||
|
config.DNSRouteInterval = *input.DNSRouteInterval
|
||||||
|
updated = true
|
||||||
|
} else if config.DNSRouteInterval == 0 {
|
||||||
|
config.DNSRouteInterval = dynamic.DefaultInterval
|
||||||
|
log.Infof("using default DNS route interval %s", config.DNSRouteInterval)
|
||||||
|
updated = true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseURL parses and validates a service URL
|
// parseURL parses and validates a service URL
|
||||||
func parseURL(serviceName, serviceURL string) (*url.URL, error) {
|
func parseURL(serviceName, serviceURL string) (*url.URL, error) {
|
||||||
parsedMgmtURL, err := url.ParseRequestURI(serviceURL)
|
parsedMgmtURL, err := url.ParseRequestURI(serviceURL)
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
@@ -24,34 +26,52 @@ import (
|
|||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
mgm "github.com/netbirdio/netbird/management/client"
|
mgm "github.com/netbirdio/netbird/management/client"
|
||||||
mgmProto "github.com/netbirdio/netbird/management/proto"
|
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||||
|
"github.com/netbirdio/netbird/relay/auth/hmac"
|
||||||
|
relayClient "github.com/netbirdio/netbird/relay/client"
|
||||||
signal "github.com/netbirdio/netbird/signal/client"
|
signal "github.com/netbirdio/netbird/signal/client"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunClient with main logic.
|
type ConnectClient struct {
|
||||||
func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status) error {
|
ctx context.Context
|
||||||
return runClient(ctx, config, statusRecorder, MobileDependency{}, nil, nil, nil, nil)
|
config *Config
|
||||||
|
statusRecorder *peer.Status
|
||||||
|
engine *Engine
|
||||||
|
engineMutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunClientWithProbes runs the client's main logic with probes attached
|
func NewConnectClient(
|
||||||
func RunClientWithProbes(
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
config *Config,
|
config *Config,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
|
|
||||||
|
) *ConnectClient {
|
||||||
|
return &ConnectClient{
|
||||||
|
ctx: ctx,
|
||||||
|
config: config,
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
|
engineMutex: sync.Mutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run with main logic.
|
||||||
|
func (c *ConnectClient) Run() error {
|
||||||
|
return c.run(MobileDependency{}, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunWithProbes runs the client's main logic with probes attached
|
||||||
|
func (c *ConnectClient) RunWithProbes(
|
||||||
mgmProbe *Probe,
|
mgmProbe *Probe,
|
||||||
signalProbe *Probe,
|
signalProbe *Probe,
|
||||||
relayProbe *Probe,
|
relayProbe *Probe,
|
||||||
wgProbe *Probe,
|
wgProbe *Probe,
|
||||||
) error {
|
) error {
|
||||||
return runClient(ctx, config, statusRecorder, MobileDependency{}, mgmProbe, signalProbe, relayProbe, wgProbe)
|
return c.run(MobileDependency{}, mgmProbe, signalProbe, relayProbe, wgProbe)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunClientMobile with main logic on mobile system
|
// RunOnAndroid with main logic on mobile system
|
||||||
func RunClientMobile(
|
func (c *ConnectClient) RunOnAndroid(
|
||||||
ctx context.Context,
|
|
||||||
config *Config,
|
|
||||||
statusRecorder *peer.Status,
|
|
||||||
tunAdapter iface.TunAdapter,
|
tunAdapter iface.TunAdapter,
|
||||||
iFaceDiscover stdnet.ExternalIFaceDiscover,
|
iFaceDiscover stdnet.ExternalIFaceDiscover,
|
||||||
networkChangeListener listener.NetworkChangeListener,
|
networkChangeListener listener.NetworkChangeListener,
|
||||||
@@ -66,29 +86,26 @@ func RunClientMobile(
|
|||||||
HostDNSAddresses: dnsAddresses,
|
HostDNSAddresses: dnsAddresses,
|
||||||
DnsReadyListener: dnsReadyListener,
|
DnsReadyListener: dnsReadyListener,
|
||||||
}
|
}
|
||||||
return runClient(ctx, config, statusRecorder, mobileDependency, nil, nil, nil, nil)
|
return c.run(mobileDependency, nil, nil, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunClientiOS(
|
func (c *ConnectClient) RunOniOS(
|
||||||
ctx context.Context,
|
|
||||||
config *Config,
|
|
||||||
statusRecorder *peer.Status,
|
|
||||||
fileDescriptor int32,
|
fileDescriptor int32,
|
||||||
networkChangeListener listener.NetworkChangeListener,
|
networkChangeListener listener.NetworkChangeListener,
|
||||||
dnsManager dns.IosDnsManager,
|
dnsManager dns.IosDnsManager,
|
||||||
) error {
|
) error {
|
||||||
|
// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
|
||||||
|
debug.SetGCPercent(5)
|
||||||
|
|
||||||
mobileDependency := MobileDependency{
|
mobileDependency := MobileDependency{
|
||||||
FileDescriptor: fileDescriptor,
|
FileDescriptor: fileDescriptor,
|
||||||
NetworkChangeListener: networkChangeListener,
|
NetworkChangeListener: networkChangeListener,
|
||||||
DnsManager: dnsManager,
|
DnsManager: dnsManager,
|
||||||
}
|
}
|
||||||
return runClient(ctx, config, statusRecorder, mobileDependency, nil, nil, nil, nil)
|
return c.run(mobileDependency, nil, nil, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClient(
|
func (c *ConnectClient) run(
|
||||||
ctx context.Context,
|
|
||||||
config *Config,
|
|
||||||
statusRecorder *peer.Status,
|
|
||||||
mobileDependency MobileDependency,
|
mobileDependency MobileDependency,
|
||||||
mgmProbe *Probe,
|
mgmProbe *Probe,
|
||||||
signalProbe *Probe,
|
signalProbe *Probe,
|
||||||
@@ -105,7 +122,7 @@ func runClient(
|
|||||||
|
|
||||||
// Check if client was not shut down in a clean way and restore DNS config if required.
|
// Check if client was not shut down in a clean way and restore DNS config 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.
|
||||||
if err := dns.CheckUncleanShutdown(config.WgIface); err != nil {
|
if err := dns.CheckUncleanShutdown(c.config.WgIface); err != nil {
|
||||||
log.Errorf("checking unclean shutdown error: %s", err)
|
log.Errorf("checking unclean shutdown error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +136,7 @@ func runClient(
|
|||||||
Clock: backoff.SystemClock,
|
Clock: backoff.SystemClock,
|
||||||
}
|
}
|
||||||
|
|
||||||
state := CtxGetState(ctx)
|
state := CtxGetState(c.ctx)
|
||||||
defer func() {
|
defer func() {
|
||||||
s, err := state.Status()
|
s, err := state.Status()
|
||||||
if err != nil || s != StatusNeedsLogin {
|
if err != nil || s != StatusNeedsLogin {
|
||||||
@@ -128,49 +145,49 @@ func runClient(
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
wrapErr := state.Wrap
|
wrapErr := state.Wrap
|
||||||
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
|
myPrivateKey, err := wgtypes.ParseKey(c.config.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
|
log.Errorf("failed parsing Wireguard key %s: [%s]", c.config.PrivateKey, err.Error())
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var mgmTlsEnabled bool
|
var mgmTlsEnabled bool
|
||||||
if config.ManagementURL.Scheme == "https" {
|
if c.config.ManagementURL.Scheme == "https" {
|
||||||
mgmTlsEnabled = true
|
mgmTlsEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
publicSSHKey, err := ssh.GeneratePublicKey([]byte(config.SSHKey))
|
publicSSHKey, err := ssh.GeneratePublicKey([]byte(c.config.SSHKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer statusRecorder.ClientStop()
|
defer c.statusRecorder.ClientStop()
|
||||||
operation := func() error {
|
operation := func() error {
|
||||||
// if context cancelled we not start new backoff cycle
|
// if context cancelled we not start new backoff cycle
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-c.ctx.Done():
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Set(StatusConnecting)
|
state.Set(StatusConnecting)
|
||||||
|
|
||||||
engineCtx, cancel := context.WithCancel(ctx)
|
engineCtx, cancel := context.WithCancel(c.ctx)
|
||||||
defer func() {
|
defer func() {
|
||||||
statusRecorder.MarkManagementDisconnected(state.err)
|
c.statusRecorder.MarkManagementDisconnected(state.err)
|
||||||
statusRecorder.CleanLocalPeerState()
|
c.statusRecorder.CleanLocalPeerState()
|
||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Debugf("connecting to the Management service %s", config.ManagementURL.Host)
|
log.Debugf("connecting to the Management service %s", c.config.ManagementURL.Host)
|
||||||
mgmClient, err := mgm.NewClient(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
mgmClient, err := mgm.NewClient(engineCtx, c.config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapErr(gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err))
|
return wrapErr(gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err))
|
||||||
}
|
}
|
||||||
mgmNotifier := statusRecorderToMgmConnStateNotifier(statusRecorder)
|
mgmNotifier := statusRecorderToMgmConnStateNotifier(c.statusRecorder)
|
||||||
mgmClient.SetConnStateListener(mgmNotifier)
|
mgmClient.SetConnStateListener(mgmNotifier)
|
||||||
|
|
||||||
log.Debugf("connected to the Management service %s", config.ManagementURL.Host)
|
log.Debugf("connected to the Management service %s", c.config.ManagementURL.Host)
|
||||||
defer func() {
|
defer func() {
|
||||||
err = mgmClient.Close()
|
err = mgmClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -188,7 +205,7 @@ func runClient(
|
|||||||
}
|
}
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
statusRecorder.MarkManagementConnected()
|
c.statusRecorder.MarkManagementConnected()
|
||||||
|
|
||||||
localPeerState := peer.LocalPeerState{
|
localPeerState := peer.LocalPeerState{
|
||||||
IP: loginResp.GetPeerConfig().GetAddress(),
|
IP: loginResp.GetPeerConfig().GetAddress(),
|
||||||
@@ -197,18 +214,18 @@ func runClient(
|
|||||||
FQDN: loginResp.GetPeerConfig().GetFqdn(),
|
FQDN: loginResp.GetPeerConfig().GetFqdn(),
|
||||||
}
|
}
|
||||||
|
|
||||||
statusRecorder.UpdateLocalPeerState(localPeerState)
|
c.statusRecorder.UpdateLocalPeerState(localPeerState)
|
||||||
|
|
||||||
signalURL := fmt.Sprintf("%s://%s",
|
signalURL := fmt.Sprintf("%s://%s",
|
||||||
strings.ToLower(loginResp.GetWiretrusteeConfig().GetSignal().GetProtocol().String()),
|
strings.ToLower(loginResp.GetWiretrusteeConfig().GetSignal().GetProtocol().String()),
|
||||||
loginResp.GetWiretrusteeConfig().GetSignal().GetUri(),
|
loginResp.GetWiretrusteeConfig().GetSignal().GetUri(),
|
||||||
)
|
)
|
||||||
|
|
||||||
statusRecorder.UpdateSignalAddress(signalURL)
|
c.statusRecorder.UpdateSignalAddress(signalURL)
|
||||||
|
|
||||||
statusRecorder.MarkSignalDisconnected(nil)
|
c.statusRecorder.MarkSignalDisconnected(nil)
|
||||||
defer func() {
|
defer func() {
|
||||||
statusRecorder.MarkSignalDisconnected(state.err)
|
c.statusRecorder.MarkSignalDisconnected(state.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
|
||||||
@@ -224,35 +241,54 @@ func runClient(
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
signalNotifier := statusRecorderToSignalConnStateNotifier(statusRecorder)
|
signalNotifier := statusRecorderToSignalConnStateNotifier(c.statusRecorder)
|
||||||
signalClient.SetConnStateListener(signalNotifier)
|
signalClient.SetConnStateListener(signalNotifier)
|
||||||
|
|
||||||
statusRecorder.MarkSignalConnected()
|
c.statusRecorder.MarkSignalConnected()
|
||||||
|
|
||||||
|
relayURL, token := parseRelayInfo(loginResp)
|
||||||
|
relayManager := relayClient.NewManager(engineCtx, relayURL, myPrivateKey.PublicKey().String())
|
||||||
|
if relayURL != "" {
|
||||||
|
if token != nil {
|
||||||
|
relayManager.UpdateToken(token)
|
||||||
|
}
|
||||||
|
log.Infof("connecting to the Relay service %s", relayURL)
|
||||||
|
if err = relayManager.Serve(); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return wrapErr(err)
|
||||||
|
}
|
||||||
|
c.statusRecorder.SetRelayMgr(relayManager)
|
||||||
|
}
|
||||||
|
|
||||||
peerConfig := loginResp.GetPeerConfig()
|
peerConfig := loginResp.GetPeerConfig()
|
||||||
|
|
||||||
engineConfig, err := createEngineConfig(myPrivateKey, config, peerConfig)
|
engineConfig, err := createEngineConfig(myPrivateKey, c.config, peerConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
engine := NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, engineConfig, mobileDependency, statusRecorder, mgmProbe, signalProbe, relayProbe, wgProbe)
|
checks := loginResp.GetChecks()
|
||||||
err = engine.Start()
|
|
||||||
|
c.engineMutex.Lock()
|
||||||
|
c.engine = NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, mgmProbe, signalProbe, relayProbe, wgProbe, checks)
|
||||||
|
c.engineMutex.Unlock()
|
||||||
|
|
||||||
|
err = c.engine.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error while starting Netbird Connection Engine: %s", err)
|
log.Errorf("error while starting Netbird Connection Engine: %s", err)
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Print("Netbird engine started, my IP is: ", peerConfig.Address)
|
log.Infof("Netbird engine started, the IP is: %s", peerConfig.GetAddress())
|
||||||
state.Set(StatusConnected)
|
state.Set(StatusConnected)
|
||||||
|
|
||||||
<-engineCtx.Done()
|
<-engineCtx.Done()
|
||||||
statusRecorder.ClientTeardown()
|
c.statusRecorder.ClientTeardown()
|
||||||
|
|
||||||
backOff.Reset()
|
backOff.Reset()
|
||||||
|
|
||||||
err = engine.Stop()
|
err = c.engine.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed stopping engine %v", err)
|
log.Errorf("failed stopping engine %v", err)
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
@@ -267,7 +303,7 @@ func runClient(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
statusRecorder.ClientStart()
|
c.statusRecorder.ClientStart()
|
||||||
err = backoff.Retry(operation, backOff)
|
err = backoff.Retry(operation, backOff)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
|
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
|
||||||
@@ -279,8 +315,44 @@ func runClient(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseRelayInfo(resp *mgmProto.LoginResponse) (string, *hmac.Token) {
|
||||||
|
// todo remove this
|
||||||
|
if ra := peer.ForcedRelayAddress(); ra != "" {
|
||||||
|
return ra, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := resp.GetWiretrusteeConfig().GetRelay()
|
||||||
|
if msg == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var url string
|
||||||
|
if msg.GetUrls() != nil && len(msg.GetUrls()) > 0 {
|
||||||
|
url = msg.GetUrls()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
token := &hmac.Token{
|
||||||
|
Payload: msg.GetTokenPayload(),
|
||||||
|
Signature: msg.GetTokenSignature(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return url, token
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnectClient) Engine() *Engine {
|
||||||
|
var e *Engine
|
||||||
|
c.engineMutex.Lock()
|
||||||
|
e = c.engine
|
||||||
|
c.engineMutex.Unlock()
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
if config.NetworkMonitor != nil {
|
||||||
|
nm = *config.NetworkMonitor
|
||||||
|
}
|
||||||
engineConf := &EngineConfig{
|
engineConf := &EngineConfig{
|
||||||
WgIfaceName: config.WgIface,
|
WgIfaceName: config.WgIface,
|
||||||
WgAddr: peerConfig.Address,
|
WgAddr: peerConfig.Address,
|
||||||
@@ -288,12 +360,14 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe
|
|||||||
DisableIPv6Discovery: config.DisableIPv6Discovery,
|
DisableIPv6Discovery: config.DisableIPv6Discovery,
|
||||||
WgPrivateKey: key,
|
WgPrivateKey: key,
|
||||||
WgPort: config.WgPort,
|
WgPort: config.WgPort,
|
||||||
|
NetworkMonitor: nm,
|
||||||
SSHKey: []byte(config.SSHKey),
|
SSHKey: []byte(config.SSHKey),
|
||||||
NATExternalIPs: config.NATExternalIPs,
|
NATExternalIPs: config.NATExternalIPs,
|
||||||
CustomDNSAddress: config.CustomDNSAddress,
|
CustomDNSAddress: config.CustomDNSAddress,
|
||||||
RosenpassEnabled: config.RosenpassEnabled,
|
RosenpassEnabled: config.RosenpassEnabled,
|
||||||
RosenpassPermissive: config.RosenpassPermissive,
|
RosenpassPermissive: config.RosenpassPermissive,
|
||||||
ServerSSHAllowed: util.ReturnBoolWithDefaultTrue(config.ServerSSHAllowed),
|
ServerSSHAllowed: util.ReturnBoolWithDefaultTrue(config.ServerSSHAllowed),
|
||||||
|
DNSRouteInterval: config.DNSRouteInterval,
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.PreSharedKey != "" {
|
if config.PreSharedKey != "" {
|
||||||
@@ -304,6 +378,15 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe
|
|||||||
engineConf.PreSharedKey = &preSharedKey
|
engineConf.PreSharedKey = &preSharedKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
port, err := freePort(config.WgPort)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if port != config.WgPort {
|
||||||
|
log.Infof("using %d as wireguard port: %d is in use", port, config.WgPort)
|
||||||
|
}
|
||||||
|
engineConf.WgPort = port
|
||||||
|
|
||||||
return engineConf, nil
|
return engineConf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,3 +436,20 @@ func statusRecorderToSignalConnStateNotifier(statusRecorder *peer.Status) signal
|
|||||||
notifier, _ := sri.(signal.ConnStateNotifier)
|
notifier, _ := sri.(signal.ConnStateNotifier)
|
||||||
return notifier
|
return notifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func freePort(start int) (int, error) {
|
||||||
|
addr := net.UDPAddr{}
|
||||||
|
if start == 0 {
|
||||||
|
start = iface.DefaultWgPort
|
||||||
|
}
|
||||||
|
for x := start; x <= 65535; x++ {
|
||||||
|
addr.Port = x
|
||||||
|
conn, err := net.ListenUDP("udp", &addr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
return 0, errors.New("no free ports")
|
||||||
|
}
|
||||||
|
|||||||
57
client/internal/connect_test.go
Normal file
57
client/internal/connect_test.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_freePort(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
port int
|
||||||
|
want int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "available",
|
||||||
|
port: 51820,
|
||||||
|
want: 51820,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "notavailable",
|
||||||
|
port: 51830,
|
||||||
|
want: 51831,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "noports",
|
||||||
|
port: 65535,
|
||||||
|
want: 0,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
|
||||||
|
c1, err := net.ListenUDP("udp", &net.UDPAddr{Port: 51830})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("freePort error = %v", err)
|
||||||
|
}
|
||||||
|
c2, err := net.ListenUDP("udp", &net.UDPAddr{Port: 65535})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("freePort error = %v", err)
|
||||||
|
}
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := freePort(tt.port)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("freePort() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("freePort() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
c1.Close()
|
||||||
|
c2.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
6
client/internal/dns/consts_freebsd.go
Normal file
6
client/internal/dns/consts_freebsd.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
const (
|
||||||
|
fileUncleanShutdownResolvConfLocation = "/var/db/netbird/resolv.conf"
|
||||||
|
fileUncleanShutdownManagerTypeLocation = "/var/db/netbird/manager"
|
||||||
|
)
|
||||||
8
client/internal/dns/consts_linux.go
Normal file
8
client/internal/dns/consts_linux.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !android
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
const (
|
||||||
|
fileUncleanShutdownResolvConfLocation = "/var/lib/netbird/resolv.conf"
|
||||||
|
fileUncleanShutdownManagerTypeLocation = "/var/lib/netbird/manager"
|
||||||
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -47,24 +47,20 @@ func (f *fileConfigurator) supportCustomPort() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig) error {
|
func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig) error {
|
||||||
backupFileExist := false
|
backupFileExist := f.isBackupFileExist()
|
||||||
_, err := os.Stat(fileDefaultResolvConfBackupLocation)
|
|
||||||
if err == nil {
|
|
||||||
backupFileExist = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !config.RouteAll {
|
if !config.RouteAll {
|
||||||
if backupFileExist {
|
if backupFileExist {
|
||||||
err = f.restore()
|
f.repair.stopWatchFileChanges()
|
||||||
|
err := f.restore()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to configure DNS for this peer using file manager without a Primary nameserver group. Restoring the original file return err: %w", err)
|
return fmt.Errorf("restoring the original resolv.conf file return err: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured")
|
return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !backupFileExist {
|
if !backupFileExist {
|
||||||
err = f.backup()
|
err := f.backup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to backup the resolv.conf file: %w", err)
|
return fmt.Errorf("unable to backup the resolv.conf file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -184,6 +180,11 @@ func (f *fileConfigurator) restoreUncleanShutdownDNS(storedDNSAddress *netip.Add
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) isBackupFileExist() bool {
|
||||||
|
_, err := os.Stat(fileDefaultResolvConfBackupLocation)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func restoreResolvConfFile() error {
|
func restoreResolvConfFile() error {
|
||||||
log.Debugf("restoring unclean shutdown: restoring %s from %s", defaultResolvConfPath, fileUncleanShutdownResolvConfLocation)
|
log.Debugf("restoring unclean shutdown: restoring %s from %s", defaultResolvConfPath, fileUncleanShutdownResolvConfLocation)
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ func getOSDNSManagerType() (osManagerType, error) {
|
|||||||
if strings.Contains(text, "NetworkManager") && isDbusListenerRunning(networkManagerDest, networkManagerDbusObjectNode) && isNetworkManagerSupported() {
|
if strings.Contains(text, "NetworkManager") && isDbusListenerRunning(networkManagerDest, networkManagerDbusObjectNode) && isNetworkManagerSupported() {
|
||||||
return networkManager, nil
|
return networkManager, nil
|
||||||
}
|
}
|
||||||
if strings.Contains(text, "systemd-resolved") && isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) {
|
if strings.Contains(text, "systemd-resolved") && isSystemdResolvedRunning() {
|
||||||
if checkStub() {
|
if checkStub() {
|
||||||
return systemdManager, nil
|
return systemdManager, nil
|
||||||
} else {
|
} else {
|
||||||
@@ -116,16 +116,10 @@ func getOSDNSManagerType() (osManagerType, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if strings.Contains(text, "resolvconf") {
|
if strings.Contains(text, "resolvconf") {
|
||||||
if isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) {
|
if isSystemdResolveConfMode() {
|
||||||
var value string
|
return systemdManager, nil
|
||||||
err = getSystemdDbusProperty(systemdDbusResolvConfModeProperty, &value)
|
|
||||||
if err == nil {
|
|
||||||
if value == systemdDbusResolvConfModeForeign {
|
|
||||||
return systemdManager, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Errorf("got an error while checking systemd resolv conf mode, error: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolvConfManager, nil
|
return resolvConfManager, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
63
client/internal/dns/hosts_dns_holder.go
Normal file
63
client/internal/dns/hosts_dns_holder.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hostsDNSHolder struct {
|
||||||
|
unprotectedDNSList map[string]struct{}
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHostsDNSHolder() *hostsDNSHolder {
|
||||||
|
return &hostsDNSHolder{
|
||||||
|
unprotectedDNSList: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hostsDNSHolder) set(list []string) {
|
||||||
|
h.mutex.Lock()
|
||||||
|
h.unprotectedDNSList = make(map[string]struct{})
|
||||||
|
for _, dns := range list {
|
||||||
|
dnsAddr, err := h.normalizeAddress(dns)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h.unprotectedDNSList[dnsAddr] = struct{}{}
|
||||||
|
}
|
||||||
|
h.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hostsDNSHolder) get() map[string]struct{} {
|
||||||
|
h.mutex.RLock()
|
||||||
|
l := h.unprotectedDNSList
|
||||||
|
h.mutex.RUnlock()
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:unused
|
||||||
|
func (h *hostsDNSHolder) isContain(upstream string) bool {
|
||||||
|
h.mutex.RLock()
|
||||||
|
defer h.mutex.RUnlock()
|
||||||
|
|
||||||
|
_, ok := h.unprotectedDNSList[upstream]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hostsDNSHolder) normalizeAddress(addr string) (string, error) {
|
||||||
|
a, err := netip.ParseAddr(addr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("invalid upstream IP address: %s, error: %s", addr, err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Is4() {
|
||||||
|
return fmt.Sprintf("%s:53", addr), nil
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("[%s]:53", addr), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,8 @@ func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
response := d.lookupRecord(r)
|
response := d.lookupRecord(r)
|
||||||
if response != nil {
|
if response != nil {
|
||||||
replyMessage.Answer = append(replyMessage.Answer, response)
|
replyMessage.Answer = append(replyMessage.Answer, response)
|
||||||
|
} else {
|
||||||
|
replyMessage.Rcode = dns.RcodeNameError
|
||||||
}
|
}
|
||||||
|
|
||||||
err := w.WriteMsg(replyMessage)
|
err := w.WriteMsg(replyMessage)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -54,9 +55,8 @@ type DefaultServer struct {
|
|||||||
currentConfig HostDNSConfig
|
currentConfig HostDNSConfig
|
||||||
|
|
||||||
// permanent related properties
|
// permanent related properties
|
||||||
permanent bool
|
permanent bool
|
||||||
hostsDnsList []string
|
hostsDNSHolder *hostsDNSHolder
|
||||||
hostsDnsListLock sync.Mutex
|
|
||||||
|
|
||||||
// make sense on mobile only
|
// make sense on mobile only
|
||||||
searchDomainNotifier *notifier
|
searchDomainNotifier *notifier
|
||||||
@@ -113,8 +113,8 @@ func NewDefaultServerPermanentUpstream(
|
|||||||
) *DefaultServer {
|
) *DefaultServer {
|
||||||
log.Debugf("host dns address list is: %v", hostsDnsList)
|
log.Debugf("host dns address list is: %v", hostsDnsList)
|
||||||
ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface), statusRecorder)
|
ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface), statusRecorder)
|
||||||
|
ds.hostsDNSHolder.set(hostsDnsList)
|
||||||
ds.permanent = true
|
ds.permanent = true
|
||||||
ds.hostsDnsList = hostsDnsList
|
|
||||||
ds.addHostRootZone()
|
ds.addHostRootZone()
|
||||||
ds.currentConfig = dnsConfigToHostDNSConfig(config, ds.service.RuntimeIP(), ds.service.RuntimePort())
|
ds.currentConfig = dnsConfigToHostDNSConfig(config, ds.service.RuntimeIP(), ds.service.RuntimePort())
|
||||||
ds.searchDomainNotifier = newNotifier(ds.SearchDomains())
|
ds.searchDomainNotifier = newNotifier(ds.SearchDomains())
|
||||||
@@ -147,6 +147,7 @@ func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService servi
|
|||||||
},
|
},
|
||||||
wgInterface: wgInterface,
|
wgInterface: wgInterface,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
|
hostsDNSHolder: newHostsDNSHolder(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultServer
|
return defaultServer
|
||||||
@@ -202,10 +203,8 @@ func (s *DefaultServer) Stop() {
|
|||||||
// OnUpdatedHostDNSServer update the DNS servers addresses for root zones
|
// OnUpdatedHostDNSServer update the DNS servers addresses for root zones
|
||||||
// It will be applied if the mgm server do not enforce DNS settings for root zone
|
// It will be applied if the mgm server do not enforce DNS settings for root zone
|
||||||
func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) {
|
func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) {
|
||||||
s.hostsDnsListLock.Lock()
|
s.hostsDNSHolder.set(hostsDnsList)
|
||||||
defer s.hostsDnsListLock.Unlock()
|
|
||||||
|
|
||||||
s.hostsDnsList = hostsDnsList
|
|
||||||
_, ok := s.dnsMuxMap[nbdns.RootZone]
|
_, ok := s.dnsMuxMap[nbdns.RootZone]
|
||||||
if ok {
|
if ok {
|
||||||
log.Debugf("on new host DNS config but skip to apply it")
|
log.Debugf("on new host DNS config but skip to apply it")
|
||||||
@@ -374,6 +373,7 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam
|
|||||||
s.wgInterface.Address().IP,
|
s.wgInterface.Address().IP,
|
||||||
s.wgInterface.Address().Network,
|
s.wgInterface.Address().Network,
|
||||||
s.statusRecorder,
|
s.statusRecorder,
|
||||||
|
s.hostsDNSHolder,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to create a new upstream resolver, error: %v", err)
|
return nil, fmt.Errorf("unable to create a new upstream resolver, error: %v", err)
|
||||||
@@ -452,9 +452,7 @@ func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) {
|
|||||||
_, found := muxUpdateMap[key]
|
_, found := muxUpdateMap[key]
|
||||||
if !found {
|
if !found {
|
||||||
if !isContainRootUpdate && key == nbdns.RootZone {
|
if !isContainRootUpdate && key == nbdns.RootZone {
|
||||||
s.hostsDnsListLock.Lock()
|
|
||||||
s.addHostRootZone()
|
s.addHostRootZone()
|
||||||
s.hostsDnsListLock.Unlock()
|
|
||||||
existingHandler.stop()
|
existingHandler.stop()
|
||||||
} else {
|
} else {
|
||||||
existingHandler.stop()
|
existingHandler.stop()
|
||||||
@@ -512,6 +510,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, item := range s.currentConfig.Domains {
|
for i, item := range s.currentConfig.Domains {
|
||||||
@@ -521,10 +520,15 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
removeIndex[item.Domain] = i
|
removeIndex[item.Domain] = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
|
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
|
||||||
l.Errorf("Failed to apply nameserver deactivation on the host: %v", err)
|
l.Errorf("Failed to apply nameserver deactivation on the host: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "android" && nsGroup.Primary && len(s.hostsDNSHolder.get()) > 0 {
|
||||||
|
s.addHostRootZone()
|
||||||
|
}
|
||||||
|
|
||||||
s.updateNSState(nsGroup, err, false)
|
s.updateNSState(nsGroup, err, false)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -545,6 +549,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)
|
||||||
}
|
}
|
||||||
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
|
if err := s.hostManager.applyDNSConfig(s.currentConfig); 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")
|
||||||
@@ -562,25 +567,16 @@ func (s *DefaultServer) addHostRootZone() {
|
|||||||
s.wgInterface.Address().IP,
|
s.wgInterface.Address().IP,
|
||||||
s.wgInterface.Address().Network,
|
s.wgInterface.Address().Network,
|
||||||
s.statusRecorder,
|
s.statusRecorder,
|
||||||
|
s.hostsDNSHolder,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("unable to create a new upstream resolver, error: %v", err)
|
log.Errorf("unable to create a new upstream resolver, error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handler.upstreamServers = make([]string, len(s.hostsDnsList))
|
|
||||||
for n, ua := range s.hostsDnsList {
|
|
||||||
a, err := netip.ParseAddr(ua)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("invalid upstream IP address: %s, error: %s", ua, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ipString := ua
|
handler.upstreamServers = make([]string, 0)
|
||||||
if !a.Is4() {
|
for k := range s.hostsDNSHolder.get() {
|
||||||
ipString = fmt.Sprintf("[%s]", ua)
|
handler.upstreamServers = append(handler.upstreamServers, k)
|
||||||
}
|
|
||||||
|
|
||||||
handler.upstreamServers[n] = fmt.Sprintf("%s:53", ipString)
|
|
||||||
}
|
}
|
||||||
handler.deactivate = func(error) {}
|
handler.deactivate = func(error) {}
|
||||||
handler.reactivate = func() {}
|
handler.reactivate = func() {}
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ func (w *mocWGIface) Address() iface.WGAddress {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *mocWGIface) ToInterface() *net.Interface {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
func (w *mocWGIface) GetFilter() iface.PacketFilter {
|
func (w *mocWGIface) GetFilter() iface.PacketFilter {
|
||||||
return w.filter
|
return w.filter
|
||||||
}
|
}
|
||||||
@@ -261,7 +265,7 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
wgIface, err := iface.NewWGIFace(fmt.Sprintf("utun230%d", n), fmt.Sprintf("100.66.100.%d/32", n+1), 33100, privKey.String(), iface.DefaultMTU, newNet, nil)
|
wgIface, err := iface.NewWGIFace(fmt.Sprintf("utun230%d", n), fmt.Sprintf("100.66.100.%d/32", n+1), 33100, privKey.String(), iface.DefaultMTU, newNet, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -339,7 +343,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
privKey, _ := wgtypes.GeneratePrivateKey()
|
privKey, _ := wgtypes.GeneratePrivateKey()
|
||||||
wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.1/32", 33100, privKey.String(), iface.DefaultMTU, newNet, nil)
|
wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.1/32", 33100, privKey.String(), iface.DefaultMTU, newNet, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("build interface wireguard: %v", err)
|
t.Errorf("build interface wireguard: %v", err)
|
||||||
return
|
return
|
||||||
@@ -797,7 +801,7 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
privKey, _ := wgtypes.GeneratePrivateKey()
|
privKey, _ := wgtypes.GeneratePrivateKey()
|
||||||
wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.2/24", 33100, privKey.String(), iface.DefaultMTU, newNet, nil)
|
wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.2/24", 33100, privKey.String(), iface.DefaultMTU, newNet, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("build interface wireguard: %v", err)
|
t.Fatalf("build interface wireguard: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
20
client/internal/dns/systemd_freebsd.go
Normal file
20
client/internal/dns/systemd_freebsd.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errNotImplemented = errors.New("not implemented")
|
||||||
|
|
||||||
|
func newSystemdDbusConfigurator(wgInterface string) (hostManager, error) {
|
||||||
|
return nil, fmt.Errorf("systemd dns management: %w on freebsd", errNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSystemdResolvedRunning() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSystemdResolveConfMode() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -242,3 +242,25 @@ func getSystemdDbusProperty(property string, store any) error {
|
|||||||
|
|
||||||
return v.Store(store)
|
return v.Store(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isSystemdResolvedRunning() bool {
|
||||||
|
return isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSystemdResolveConfMode() bool {
|
||||||
|
if !isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var value string
|
||||||
|
if err := getSystemdDbusProperty(systemdDbusResolvConfModeProperty, &value); err != nil {
|
||||||
|
log.Errorf("got an error while checking systemd resolv conf mode, error: %s", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if value == systemdDbusResolvConfModeForeign {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -14,11 +14,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
fileUncleanShutdownResolvConfLocation = "/var/lib/netbird/resolv.conf"
|
|
||||||
fileUncleanShutdownManagerTypeLocation = "/var/lib/netbird/manager"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CheckUncleanShutdown(wgIface string) error {
|
func CheckUncleanShutdown(wgIface string) error {
|
||||||
if _, err := os.Stat(fileUncleanShutdownResolvConfLocation); err != nil {
|
if _, err := os.Stat(fileUncleanShutdownResolvConfLocation); err != nil {
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -79,6 +78,11 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
log.WithField("question", r.Question[0]).Trace("received an upstream question")
|
log.WithField("question", r.Question[0]).Trace("received an upstream question")
|
||||||
|
// set the AuthenticatedData flag and the EDNS0 buffer size to 4096 bytes to support larger dns records
|
||||||
|
if r.Extra == nil {
|
||||||
|
r.SetEdns0(4096, false)
|
||||||
|
r.MsgHdr.AuthenticatedData = true
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-u.ctx.Done():
|
case <-u.ctx.Done():
|
||||||
@@ -260,13 +264,10 @@ func (u *upstreamResolverBase) disable(err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo test the deactivation logic, it seems to affect the client
|
log.Warnf("Upstream resolving is Disabled for %v", reactivatePeriod)
|
||||||
if runtime.GOOS != "ios" {
|
u.deactivate(err)
|
||||||
log.Warnf("Upstream resolving is Disabled for %v", reactivatePeriod)
|
u.disabled = true
|
||||||
u.deactivate(err)
|
go u.waitUntilResponse()
|
||||||
u.disabled = true
|
|
||||||
go u.waitUntilResponse()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverBase) testNameserver(server string) error {
|
func (u *upstreamResolverBase) testNameserver(server string) error {
|
||||||
|
|||||||
84
client/internal/dns/upstream_android.go
Normal file
84
client/internal/dns/upstream_android.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type upstreamResolver struct {
|
||||||
|
*upstreamResolverBase
|
||||||
|
hostsDNSHolder *hostsDNSHolder
|
||||||
|
}
|
||||||
|
|
||||||
|
// newUpstreamResolver in Android we need to distinguish the DNS servers to available through VPN or outside of VPN
|
||||||
|
// In case if the assigned DNS address is available only in the protected network then the resolver will time out at the
|
||||||
|
// first time, and we need to wait for a while to start to use again the proper DNS resolver.
|
||||||
|
func newUpstreamResolver(
|
||||||
|
ctx context.Context,
|
||||||
|
_ string,
|
||||||
|
_ net.IP,
|
||||||
|
_ *net.IPNet,
|
||||||
|
statusRecorder *peer.Status,
|
||||||
|
hostsDNSHolder *hostsDNSHolder,
|
||||||
|
) (*upstreamResolver, error) {
|
||||||
|
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder)
|
||||||
|
c := &upstreamResolver{
|
||||||
|
upstreamResolverBase: upstreamResolverBase,
|
||||||
|
hostsDNSHolder: hostsDNSHolder,
|
||||||
|
}
|
||||||
|
upstreamResolverBase.upstreamClient = c
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// exchange in case of Android if the upstream is a local resolver then we do not need to mark the socket as protected.
|
||||||
|
// In other case the DNS resolvation goes through the VPN, so we need to force to use the
|
||||||
|
func (u *upstreamResolver) exchange(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
|
||||||
|
if u.isLocalResolver(upstream) {
|
||||||
|
return u.exchangeWithoutVPN(ctx, upstream, r)
|
||||||
|
} else {
|
||||||
|
return u.exchangeWithinVPN(ctx, upstream, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolver) exchangeWithinVPN(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
|
||||||
|
upstreamExchangeClient := &dns.Client{}
|
||||||
|
return upstreamExchangeClient.ExchangeContext(ctx, r, upstream)
|
||||||
|
}
|
||||||
|
|
||||||
|
// exchangeWithoutVPN protect the UDP socket by Android SDK to avoid to goes through the VPN
|
||||||
|
func (u *upstreamResolver) exchangeWithoutVPN(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
|
||||||
|
timeout := upstreamTimeout
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
timeout = time.Until(deadline)
|
||||||
|
}
|
||||||
|
dialTimeout := timeout
|
||||||
|
|
||||||
|
nbDialer := nbnet.NewDialer()
|
||||||
|
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
Control: func(network, address string, c syscall.RawConn) error {
|
||||||
|
return nbDialer.Control(network, address, c)
|
||||||
|
},
|
||||||
|
Timeout: dialTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
upstreamExchangeClient := &dns.Client{
|
||||||
|
Dialer: dialer,
|
||||||
|
}
|
||||||
|
|
||||||
|
return upstreamExchangeClient.Exchange(r, upstream)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolver) isLocalResolver(upstream string) bool {
|
||||||
|
if u.hostsDNSHolder.isContain(upstream) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !ios
|
//go:build !android && !ios
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type upstreamResolverNonIOS struct {
|
type upstreamResolver struct {
|
||||||
*upstreamResolverBase
|
*upstreamResolverBase
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,16 +22,17 @@ func newUpstreamResolver(
|
|||||||
_ net.IP,
|
_ net.IP,
|
||||||
_ *net.IPNet,
|
_ *net.IPNet,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
) (*upstreamResolverNonIOS, error) {
|
_ *hostsDNSHolder,
|
||||||
|
) (*upstreamResolver, error) {
|
||||||
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder)
|
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder)
|
||||||
nonIOS := &upstreamResolverNonIOS{
|
nonIOS := &upstreamResolver{
|
||||||
upstreamResolverBase: upstreamResolverBase,
|
upstreamResolverBase: upstreamResolverBase,
|
||||||
}
|
}
|
||||||
upstreamResolverBase.upstreamClient = nonIOS
|
upstreamResolverBase.upstreamClient = nonIOS
|
||||||
return nonIOS, nil
|
return nonIOS, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverNonIOS) exchange(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
|
func (u *upstreamResolver) exchange(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
|
||||||
upstreamExchangeClient := &dns.Client{}
|
upstreamExchangeClient := &dns.Client{}
|
||||||
return upstreamExchangeClient.ExchangeContext(ctx, r, upstream)
|
return upstreamExchangeClient.ExchangeContext(ctx, r, upstream)
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ func newUpstreamResolver(
|
|||||||
ip net.IP,
|
ip net.IP,
|
||||||
net *net.IPNet,
|
net *net.IPNet,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
|
_ *hostsDNSHolder,
|
||||||
) (*upstreamResolverIOS, error) {
|
) (*upstreamResolverIOS, error) {
|
||||||
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder)
|
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder)
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
|
|||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil)
|
resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil, nil)
|
||||||
resolver.upstreamServers = testCase.InputServers
|
resolver.upstreamServers = testCase.InputServers
|
||||||
resolver.upstreamTimeout = testCase.timeout
|
resolver.upstreamTimeout = testCase.timeout
|
||||||
if testCase.cancelCTX {
|
if testCase.cancelCTX {
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
import "github.com/netbirdio/netbird/iface"
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
)
|
||||||
|
|
||||||
// WGIface defines subset methods of interface required for manager
|
// WGIface defines subset methods of interface required for manager
|
||||||
type WGIface interface {
|
type WGIface interface {
|
||||||
Name() string
|
Name() string
|
||||||
Address() iface.WGAddress
|
Address() iface.WGAddress
|
||||||
|
ToInterface() *net.Interface
|
||||||
IsUserspaceBind() bool
|
IsUserspaceBind() bool
|
||||||
GetFilter() iface.PacketFilter
|
GetFilter() iface.PacketFilter
|
||||||
GetDevice() *iface.DeviceWrapper
|
GetDevice() *iface.DeviceWrapper
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/ice/v3"
|
"github.com/pion/ice/v3"
|
||||||
@@ -21,21 +25,29 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/firewall/manager"
|
"github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"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/networkmonitor"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"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/wgproxy"
|
"github.com/netbirdio/netbird/client/internal/wgproxy"
|
||||||
nbssh "github.com/netbirdio/netbird/client/ssh"
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
"github.com/netbirdio/netbird/iface/bind"
|
"github.com/netbirdio/netbird/iface/bind"
|
||||||
mgm "github.com/netbirdio/netbird/management/client"
|
mgm "github.com/netbirdio/netbird/management/client"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
mgmProto "github.com/netbirdio/netbird/management/proto"
|
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||||
|
auth "github.com/netbirdio/netbird/relay/auth/hmac"
|
||||||
|
relayClient "github.com/netbirdio/netbird/relay/client"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
signal "github.com/netbirdio/netbird/signal/client"
|
signal "github.com/netbirdio/netbird/signal/client"
|
||||||
sProto "github.com/netbirdio/netbird/signal/proto"
|
sProto "github.com/netbirdio/netbird/signal/proto"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PeerConnectionTimeoutMax is a timeout of an initial connection attempt to a remote peer.
|
// PeerConnectionTimeoutMax is a timeout of an initial connection attempt to a remote peer.
|
||||||
@@ -60,6 +72,9 @@ type EngineConfig struct {
|
|||||||
// WgPrivateKey is a Wireguard private key of our peer (it MUST never leave the machine)
|
// WgPrivateKey is a Wireguard private key of our peer (it MUST never leave the machine)
|
||||||
WgPrivateKey wgtypes.Key
|
WgPrivateKey wgtypes.Key
|
||||||
|
|
||||||
|
// NetworkMonitor is a flag to enable network monitoring
|
||||||
|
NetworkMonitor bool
|
||||||
|
|
||||||
// IFaceBlackList is a list of network interfaces to ignore when discovering connection candidates (ICE related)
|
// IFaceBlackList is a list of network interfaces to ignore when discovering connection candidates (ICE related)
|
||||||
IFaceBlackList []string
|
IFaceBlackList []string
|
||||||
DisableIPv6Discovery bool
|
DisableIPv6Discovery bool
|
||||||
@@ -83,19 +98,22 @@ type EngineConfig struct {
|
|||||||
RosenpassPermissive bool
|
RosenpassPermissive bool
|
||||||
|
|
||||||
ServerSSHAllowed bool
|
ServerSSHAllowed bool
|
||||||
|
|
||||||
|
DNSRouteInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
|
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
// signal is a Signal Service client
|
// signal is a Signal Service client
|
||||||
signal signal.Client
|
signal signal.Client
|
||||||
|
signaler *peer.Signaler
|
||||||
// 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
|
peerConns map[string]*peer.Conn
|
||||||
|
|
||||||
beforePeerHook peer.BeforeAddPeerHookFunc
|
beforePeerHook nbnet.AddHookFunc
|
||||||
afterPeerHook peer.AfterRemovePeerHookFunc
|
afterPeerHook nbnet.RemoveHookFunc
|
||||||
|
|
||||||
// rpManager is a Rosenpass manager
|
// rpManager is a Rosenpass manager
|
||||||
rpManager *rosenpass.Manager
|
rpManager *rosenpass.Manager
|
||||||
@@ -109,13 +127,20 @@ type Engine struct {
|
|||||||
// STUNs is a list of STUN servers used by ICE
|
// STUNs is a list of STUN servers used by ICE
|
||||||
STUNs []*stun.URI
|
STUNs []*stun.URI
|
||||||
// TURNs is a list of STUN servers used by ICE
|
// TURNs is a list of STUN servers used by ICE
|
||||||
TURNs []*stun.URI
|
TURNs []*stun.URI
|
||||||
|
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
|
||||||
|
clientCancel context.CancelFunc
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
ctx context.Context
|
wgInterface iface.IWGIface
|
||||||
|
|
||||||
wgInterface *iface.WGIface
|
|
||||||
wgProxyFactory *wgproxy.Factory
|
wgProxyFactory *wgproxy.Factory
|
||||||
|
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *bind.UniversalUDPMuxDefault
|
||||||
@@ -123,6 +148,8 @@ type Engine struct {
|
|||||||
// networkSerial is the latest CurrentSerial (state ID) of the network sent by the Management service
|
// networkSerial is the latest CurrentSerial (state ID) of the network sent by the Management service
|
||||||
networkSerial uint64
|
networkSerial uint64
|
||||||
|
|
||||||
|
networkMonitor *networkmonitor.NetworkMonitor
|
||||||
|
|
||||||
sshServerFunc func(hostKeyPEM []byte, addr string) (nbssh.Server, error)
|
sshServerFunc func(hostKeyPEM []byte, addr string) (nbssh.Server, error)
|
||||||
sshServer nbssh.Server
|
sshServer nbssh.Server
|
||||||
|
|
||||||
@@ -138,6 +165,11 @@ type Engine struct {
|
|||||||
signalProbe *Probe
|
signalProbe *Probe
|
||||||
relayProbe *Probe
|
relayProbe *Probe
|
||||||
wgProbe *Probe
|
wgProbe *Probe
|
||||||
|
|
||||||
|
// checks are the client-applied posture checks that need to be evaluated on the client
|
||||||
|
checks []*mgmProto.Checks
|
||||||
|
|
||||||
|
relayManager *relayClient.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
// Peer is an instance of the Connection Peer
|
// Peer is an instance of the Connection Peer
|
||||||
@@ -148,19 +180,22 @@ type Peer struct {
|
|||||||
|
|
||||||
// NewEngine creates a new Connection Engine
|
// NewEngine creates a new Connection Engine
|
||||||
func NewEngine(
|
func NewEngine(
|
||||||
ctx context.Context,
|
clientCtx context.Context,
|
||||||
cancel context.CancelFunc,
|
clientCancel context.CancelFunc,
|
||||||
signalClient signal.Client,
|
signalClient signal.Client,
|
||||||
mgmClient mgm.Client,
|
mgmClient mgm.Client,
|
||||||
|
relayManager *relayClient.Manager,
|
||||||
config *EngineConfig,
|
config *EngineConfig,
|
||||||
mobileDep MobileDependency,
|
mobileDep MobileDependency,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
|
checks []*mgmProto.Checks,
|
||||||
) *Engine {
|
) *Engine {
|
||||||
return NewEngineWithProbes(
|
return NewEngineWithProbes(
|
||||||
ctx,
|
clientCtx,
|
||||||
cancel,
|
clientCancel,
|
||||||
signalClient,
|
signalClient,
|
||||||
mgmClient,
|
mgmClient,
|
||||||
|
relayManager,
|
||||||
config,
|
config,
|
||||||
mobileDep,
|
mobileDep,
|
||||||
statusRecorder,
|
statusRecorder,
|
||||||
@@ -168,15 +203,17 @@ func NewEngine(
|
|||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
|
checks,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEngineWithProbes creates a new Connection Engine with probes attached
|
// NewEngineWithProbes creates a new Connection Engine with probes attached
|
||||||
func NewEngineWithProbes(
|
func NewEngineWithProbes(
|
||||||
ctx context.Context,
|
clientCtx context.Context,
|
||||||
cancel context.CancelFunc,
|
clientCancel context.CancelFunc,
|
||||||
signalClient signal.Client,
|
signalClient signal.Client,
|
||||||
mgmClient mgm.Client,
|
mgmClient mgm.Client,
|
||||||
|
relayManager *relayClient.Manager,
|
||||||
config *EngineConfig,
|
config *EngineConfig,
|
||||||
mobileDep MobileDependency,
|
mobileDep MobileDependency,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
@@ -184,12 +221,15 @@ func NewEngineWithProbes(
|
|||||||
signalProbe *Probe,
|
signalProbe *Probe,
|
||||||
relayProbe *Probe,
|
relayProbe *Probe,
|
||||||
wgProbe *Probe,
|
wgProbe *Probe,
|
||||||
|
checks []*mgmProto.Checks,
|
||||||
) *Engine {
|
) *Engine {
|
||||||
return &Engine{
|
return &Engine{
|
||||||
ctx: ctx,
|
clientCtx: clientCtx,
|
||||||
cancel: cancel,
|
clientCancel: clientCancel,
|
||||||
signal: signalClient,
|
signal: signalClient,
|
||||||
|
signaler: peer.NewSignaler(signalClient, config.WgPrivateKey),
|
||||||
mgmClient: mgmClient,
|
mgmClient: mgmClient,
|
||||||
|
relayManager: relayManager,
|
||||||
peerConns: make(map[string]*peer.Conn),
|
peerConns: make(map[string]*peer.Conn),
|
||||||
syncMsgMux: &sync.Mutex{},
|
syncMsgMux: &sync.Mutex{},
|
||||||
config: config,
|
config: config,
|
||||||
@@ -199,11 +239,11 @@ func NewEngineWithProbes(
|
|||||||
networkSerial: 0,
|
networkSerial: 0,
|
||||||
sshServerFunc: nbssh.DefaultSSHServer,
|
sshServerFunc: nbssh.DefaultSSHServer,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
wgProxyFactory: wgproxy.NewFactory(config.WgPort),
|
|
||||||
mgmProbe: mgmProbe,
|
mgmProbe: mgmProbe,
|
||||||
signalProbe: signalProbe,
|
signalProbe: signalProbe,
|
||||||
relayProbe: relayProbe,
|
relayProbe: relayProbe,
|
||||||
wgProbe: wgProbe,
|
wgProbe: wgProbe,
|
||||||
|
checks: checks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,13 +251,27 @@ func (e *Engine) Stop() error {
|
|||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
if e.cancel != nil {
|
||||||
|
e.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopping network monitor first to avoid starting the engine again
|
||||||
|
if e.networkMonitor != nil {
|
||||||
|
e.networkMonitor.Stop()
|
||||||
|
}
|
||||||
|
log.Info("Network monitor: stopped")
|
||||||
|
|
||||||
err := e.removeAllPeers()
|
err := e.removeAllPeers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.clientRoutesMu.Lock()
|
||||||
|
e.clientRoutes = nil
|
||||||
|
e.clientRoutesMu.Unlock()
|
||||||
|
|
||||||
// very ugly but we want to remove peers from the WireGuard interface first before removing interface.
|
// very ugly but we want to remove peers from the WireGuard interface first before removing interface.
|
||||||
// Removing peers happens in the conn.CLose() asynchronously
|
// Removing peers happens in the conn.Close() asynchronously
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
e.close()
|
e.close()
|
||||||
@@ -232,6 +286,11 @@ func (e *Engine) Start() error {
|
|||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
if e.cancel != nil {
|
||||||
|
e.cancel()
|
||||||
|
}
|
||||||
|
e.ctx, e.cancel = context.WithCancel(e.clientCtx)
|
||||||
|
|
||||||
wgIface, err := e.newWgIface()
|
wgIface, err := e.newWgIface()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed creating wireguard interface instance %s: [%s]", e.config.WgIfaceName, err)
|
log.Errorf("failed creating wireguard interface instance %s: [%s]", e.config.WgIfaceName, err)
|
||||||
@@ -239,6 +298,9 @@ func (e *Engine) Start() error {
|
|||||||
}
|
}
|
||||||
e.wgInterface = wgIface
|
e.wgInterface = wgIface
|
||||||
|
|
||||||
|
userspace := e.wgInterface.IsUserspaceBind()
|
||||||
|
e.wgProxyFactory = wgproxy.NewFactory(e.ctx, userspace, e.config.WgPort)
|
||||||
|
|
||||||
if e.config.RosenpassEnabled {
|
if e.config.RosenpassEnabled {
|
||||||
log.Infof("rosenpass is enabled")
|
log.Infof("rosenpass is enabled")
|
||||||
if e.config.RosenpassPermissive {
|
if e.config.RosenpassPermissive {
|
||||||
@@ -263,7 +325,7 @@ func (e *Engine) Start() error {
|
|||||||
}
|
}
|
||||||
e.dnsServer = dnsServer
|
e.dnsServer = dnsServer
|
||||||
|
|
||||||
e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder, initialRoutes)
|
e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.config.DNSRouteInterval, e.wgInterface, e.statusRecorder, initialRoutes)
|
||||||
beforePeerHook, afterPeerHook, err := e.routeManager.Init()
|
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)
|
||||||
@@ -315,6 +377,9 @@ func (e *Engine) Start() error {
|
|||||||
e.receiveManagementEvents()
|
e.receiveManagementEvents()
|
||||||
e.receiveProbeEvents()
|
e.receiveProbeEvents()
|
||||||
|
|
||||||
|
// starting network monitor at the very last to avoid disruptions
|
||||||
|
e.startNetworkMonitor()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,83 +474,49 @@ func (e *Engine) removePeer(peerKey string) error {
|
|||||||
conn, exists := e.peerConns[peerKey]
|
conn, exists := e.peerConns[peerKey]
|
||||||
if exists {
|
if exists {
|
||||||
delete(e.peerConns, peerKey)
|
delete(e.peerConns, peerKey)
|
||||||
err := conn.Close()
|
conn.Close()
|
||||||
if err != nil {
|
|
||||||
switch err.(type) {
|
|
||||||
case *peer.ConnectionAlreadyClosedError:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalCandidate(candidate ice.Candidate, myKey wgtypes.Key, remoteKey wgtypes.Key, s signal.Client) error {
|
|
||||||
err := s.Send(&sProto.Message{
|
|
||||||
Key: myKey.PublicKey().String(),
|
|
||||||
RemoteKey: remoteKey.String(),
|
|
||||||
Body: &sProto.Body{
|
|
||||||
Type: sProto.Body_CANDIDATE,
|
|
||||||
Payload: candidate.Marshal(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendSignal(message *sProto.Message, s signal.Client) error {
|
|
||||||
return s.Send(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignalOfferAnswer signals either an offer or an answer to remote peer
|
|
||||||
func SignalOfferAnswer(offerAnswer peer.OfferAnswer, myKey wgtypes.Key, remoteKey wgtypes.Key, s signal.Client,
|
|
||||||
isAnswer bool) error {
|
|
||||||
var t sProto.Body_Type
|
|
||||||
if isAnswer {
|
|
||||||
t = sProto.Body_ANSWER
|
|
||||||
} else {
|
|
||||||
t = sProto.Body_OFFER
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, err := signal.MarshalCredential(myKey, offerAnswer.WgListenPort, remoteKey, &signal.Credential{
|
|
||||||
UFrag: offerAnswer.IceCredentials.UFrag,
|
|
||||||
Pwd: offerAnswer.IceCredentials.Pwd,
|
|
||||||
}, t, offerAnswer.RosenpassPubKey, offerAnswer.RosenpassAddr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Send(msg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
if update.GetWiretrusteeConfig() != nil {
|
if update.GetWiretrusteeConfig() != nil {
|
||||||
err := e.updateTURNs(update.GetWiretrusteeConfig().GetTurns())
|
wCfg := update.GetWiretrusteeConfig()
|
||||||
|
err := e.updateTURNs(wCfg.GetTurns())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = e.updateSTUNs(update.GetWiretrusteeConfig().GetStuns())
|
err = e.updateSTUNs(wCfg.GetStuns())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var stunTurn []*stun.URI
|
||||||
|
stunTurn = append(stunTurn, e.STUNs...)
|
||||||
|
stunTurn = append(stunTurn, e.TURNs...)
|
||||||
|
e.StunTurn.Store(stunTurn)
|
||||||
|
|
||||||
|
relayMsg := wCfg.GetRelay()
|
||||||
|
if relayMsg != nil {
|
||||||
|
c := &auth.Token{
|
||||||
|
Payload: relayMsg.GetTokenPayload(),
|
||||||
|
Signature: relayMsg.GetTokenSignature(),
|
||||||
|
}
|
||||||
|
e.relayManager.UpdateToken(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo update relay address in the relay manager
|
||||||
// todo update signal
|
// todo update signal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := e.updateChecksIfNew(update.Checks); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if update.GetNetworkMap() != nil {
|
if update.GetNetworkMap() != nil {
|
||||||
// only apply new changes and ignore old ones
|
// only apply new changes and ignore old ones
|
||||||
err := e.updateNetworkMap(update.GetNetworkMap())
|
err := e.updateNetworkMap(update.GetNetworkMap())
|
||||||
@@ -493,7 +524,27 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateChecksIfNew updates checks if there are changes and sync new meta with management
|
||||||
|
func (e *Engine) updateChecksIfNew(checks []*mgmProto.Checks) error {
|
||||||
|
// if checks are equal, we skip the update
|
||||||
|
if isChecksEqual(e.checks, checks) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
e.checks = checks
|
||||||
|
|
||||||
|
info, err := system.GetInfoWithChecks(e.ctx, checks)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to get system info with checks: %v", err)
|
||||||
|
info = system.GetInfo(e.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.mgmClient.SyncMeta(info); err != nil {
|
||||||
|
log.Errorf("could not sync meta: error %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,8 +560,8 @@ func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error {
|
|||||||
} else {
|
} else {
|
||||||
|
|
||||||
if sshConf.GetSshEnabled() {
|
if sshConf.GetSshEnabled() {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" || runtime.GOOS == "freebsd" {
|
||||||
log.Warnf("running SSH server on Windows is not supported")
|
log.Warnf("running SSH server on %s is not supported", runtime.GOOS)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// start SSH server if it wasn't running
|
// start SSH server if it wasn't running
|
||||||
@@ -583,12 +634,19 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
|
|||||||
// E.g. when a new peer has been registered and we are allowed to connect to it.
|
// E.g. when a new peer has been registered and we are allowed to connect to it.
|
||||||
func (e *Engine) receiveManagementEvents() {
|
func (e *Engine) receiveManagementEvents() {
|
||||||
go func() {
|
go func() {
|
||||||
err := e.mgmClient.Sync(e.handleSync)
|
info, err := system.GetInfoWithChecks(e.ctx, e.checks)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to get system info with checks: %v", err)
|
||||||
|
info = system.GetInfo(e.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// err = e.mgmClient.Sync(info, e.handleSync)
|
||||||
|
err = e.mgmClient.Sync(e.ctx, info, e.handleSync)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// happens if management is unavailable for a long time.
|
// happens if management is unavailable for a long time.
|
||||||
// We want to cancel the operation of the whole client
|
// We want to cancel the operation of the whole client
|
||||||
_ = CtxGetState(e.ctx).Wrap(ErrResetConnection)
|
_ = CtxGetState(e.ctx).Wrap(ErrResetConnection)
|
||||||
e.cancel()
|
e.clientCancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Debugf("stopped receiving updates from Management Service")
|
log.Debugf("stopped receiving updates from Management Service")
|
||||||
@@ -650,6 +708,20 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protoRoutes := networkMap.GetRoutes()
|
||||||
|
if protoRoutes == nil {
|
||||||
|
protoRoutes = []*mgmProto.Route{}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, clientRoutes, err := e.routeManager.UpdateRoutes(serial, toRoutes(protoRoutes))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to update clientRoutes, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.clientRoutesMu.Lock()
|
||||||
|
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()))
|
||||||
|
|
||||||
e.updateOfflinePeers(networkMap.GetOfflinePeers())
|
e.updateOfflinePeers(networkMap.GetOfflinePeers())
|
||||||
@@ -691,14 +763,6 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protoRoutes := networkMap.GetRoutes()
|
|
||||||
if protoRoutes == nil {
|
|
||||||
protoRoutes = []*mgmProto.Route{}
|
|
||||||
}
|
|
||||||
err := e.routeManager.UpdateRoutes(serial, toRoutes(protoRoutes))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to update routes, err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
protoDNSConfig := networkMap.GetDNSConfig()
|
protoDNSConfig := networkMap.GetDNSConfig()
|
||||||
if protoDNSConfig == nil {
|
if protoDNSConfig == nil {
|
||||||
@@ -726,15 +790,24 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
|||||||
func toRoutes(protoRoutes []*mgmProto.Route) []*route.Route {
|
func toRoutes(protoRoutes []*mgmProto.Route) []*route.Route {
|
||||||
routes := make([]*route.Route, 0)
|
routes := make([]*route.Route, 0)
|
||||||
for _, protoRoute := range protoRoutes {
|
for _, protoRoute := range protoRoutes {
|
||||||
_, prefix, _ := route.ParseNetwork(protoRoute.Network)
|
var prefix netip.Prefix
|
||||||
|
if len(protoRoute.Domains) == 0 {
|
||||||
|
var err error
|
||||||
|
if prefix, err = netip.ParsePrefix(protoRoute.Network); err != nil {
|
||||||
|
log.Errorf("Failed to parse prefix %s: %v", protoRoute.Network, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
convertedRoute := &route.Route{
|
convertedRoute := &route.Route{
|
||||||
ID: protoRoute.ID,
|
ID: route.ID(protoRoute.ID),
|
||||||
Network: prefix,
|
Network: prefix,
|
||||||
NetID: protoRoute.NetID,
|
Domains: domain.FromPunycodeList(protoRoute.Domains),
|
||||||
|
NetID: route.NetID(protoRoute.NetID),
|
||||||
NetworkType: route.NetworkType(protoRoute.NetworkType),
|
NetworkType: route.NetworkType(protoRoute.NetworkType),
|
||||||
Peer: protoRoute.Peer,
|
Peer: protoRoute.Peer,
|
||||||
Metric: int(protoRoute.Metric),
|
Metric: int(protoRoute.Metric),
|
||||||
Masquerade: protoRoute.Masquerade,
|
Masquerade: protoRoute.Masquerade,
|
||||||
|
KeepRoute: protoRoute.KeepRoute,
|
||||||
}
|
}
|
||||||
routes = append(routes, convertedRoute)
|
routes = append(routes, convertedRoute)
|
||||||
}
|
}
|
||||||
@@ -832,60 +905,13 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
|
|||||||
log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err)
|
log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go e.connWorker(conn, peerKey)
|
conn.Open()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) connWorker(conn *peer.Conn, peerKey string) {
|
|
||||||
for {
|
|
||||||
|
|
||||||
// randomize starting time a bit
|
|
||||||
min := 500
|
|
||||||
max := 2000
|
|
||||||
time.Sleep(time.Duration(rand.Intn(max-min)+min) * time.Millisecond)
|
|
||||||
|
|
||||||
// if peer has been removed -> give up
|
|
||||||
if !e.peerExists(peerKey) {
|
|
||||||
log.Debugf("peer %s doesn't exist anymore, won't retry connection", peerKey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !e.signal.Ready() {
|
|
||||||
log.Infof("signal client isn't ready, skipping connection attempt %s", peerKey)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// we might have received new STUN and TURN servers meanwhile, so update them
|
|
||||||
e.syncMsgMux.Lock()
|
|
||||||
conn.UpdateStunTurn(append(e.STUNs, e.TURNs...))
|
|
||||||
e.syncMsgMux.Unlock()
|
|
||||||
|
|
||||||
err := conn.Open()
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("connection to peer %s failed: %v", peerKey, err)
|
|
||||||
switch err.(type) {
|
|
||||||
case *peer.ConnectionClosedError:
|
|
||||||
// conn has been forced to close, so we exit the loop
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) peerExists(peerKey string) bool {
|
|
||||||
e.syncMsgMux.Lock()
|
|
||||||
defer e.syncMsgMux.Unlock()
|
|
||||||
_, ok := e.peerConns[peerKey]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, error) {
|
func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, error) {
|
||||||
log.Debugf("creating peer connection %s", pubKey)
|
log.Debugf("creating peer connection %s", pubKey)
|
||||||
var stunTurn []*stun.URI
|
|
||||||
stunTurn = append(stunTurn, e.STUNs...)
|
|
||||||
stunTurn = append(stunTurn, e.TURNs...)
|
|
||||||
|
|
||||||
wgConfig := peer.WgConfig{
|
wgConfig := peer.WgConfig{
|
||||||
RemoteKey: pubKey,
|
RemoteKey: pubKey,
|
||||||
@@ -918,53 +944,29 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, e
|
|||||||
// randomize connection timeout
|
// randomize connection timeout
|
||||||
timeout := time.Duration(rand.Intn(PeerConnectionTimeoutMax-PeerConnectionTimeoutMin)+PeerConnectionTimeoutMin) * time.Millisecond
|
timeout := time.Duration(rand.Intn(PeerConnectionTimeoutMax-PeerConnectionTimeoutMin)+PeerConnectionTimeoutMin) * time.Millisecond
|
||||||
config := peer.ConnConfig{
|
config := peer.ConnConfig{
|
||||||
Key: pubKey,
|
Key: pubKey,
|
||||||
LocalKey: e.config.WgPrivateKey.PublicKey().String(),
|
LocalKey: e.config.WgPrivateKey.PublicKey().String(),
|
||||||
StunTurn: stunTurn,
|
Timeout: timeout,
|
||||||
InterfaceBlackList: e.config.IFaceBlackList,
|
WgConfig: wgConfig,
|
||||||
DisableIPv6Discovery: e.config.DisableIPv6Discovery,
|
LocalWgPort: e.config.WgPort,
|
||||||
Timeout: timeout,
|
RosenpassPubKey: e.getRosenpassPubKey(),
|
||||||
UDPMux: e.udpMux.UDPMuxDefault,
|
RosenpassAddr: e.getRosenpassAddr(),
|
||||||
UDPMuxSrflx: e.udpMux,
|
ICEConfig: peer.ICEConfig{
|
||||||
WgConfig: wgConfig,
|
StunTurn: e.StunTurn,
|
||||||
LocalWgPort: e.config.WgPort,
|
InterfaceBlackList: e.config.IFaceBlackList,
|
||||||
NATExternalIPs: e.parseNATExternalIPMappings(),
|
DisableIPv6Discovery: e.config.DisableIPv6Discovery,
|
||||||
UserspaceBind: e.wgInterface.IsUserspaceBind(),
|
UDPMux: e.udpMux.UDPMuxDefault,
|
||||||
RosenpassPubKey: e.getRosenpassPubKey(),
|
UDPMuxSrflx: e.udpMux,
|
||||||
RosenpassAddr: e.getRosenpassAddr(),
|
NATExternalIPs: e.parseNATExternalIPMappings(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConn, err := peer.NewConn(config, e.statusRecorder, e.wgProxyFactory, e.mobileDep.TunAdapter, e.mobileDep.IFaceDiscover)
|
peerConn, err := peer.NewConn(e.ctx, config, e.statusRecorder, e.wgProxyFactory, e.signaler, e.mobileDep.IFaceDiscover, e.relayManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
wgPubKey, err := wgtypes.ParseKey(pubKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
signalOffer := func(offerAnswer peer.OfferAnswer) error {
|
|
||||||
return SignalOfferAnswer(offerAnswer, e.config.WgPrivateKey, wgPubKey, e.signal, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
signalCandidate := func(candidate ice.Candidate) error {
|
|
||||||
return signalCandidate(candidate, e.config.WgPrivateKey, wgPubKey, e.signal)
|
|
||||||
}
|
|
||||||
|
|
||||||
signalAnswer := func(offerAnswer peer.OfferAnswer) error {
|
|
||||||
return SignalOfferAnswer(offerAnswer, e.config.WgPrivateKey, wgPubKey, e.signal, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
peerConn.SetSignalCandidate(signalCandidate)
|
|
||||||
peerConn.SetSignalOffer(signalOffer)
|
|
||||||
peerConn.SetSignalAnswer(signalAnswer)
|
|
||||||
peerConn.SetSendSignalMessage(func(message *sProto.Message) error {
|
|
||||||
return sendSignal(message, e.signal)
|
|
||||||
})
|
|
||||||
|
|
||||||
if e.rpManager != nil {
|
if e.rpManager != nil {
|
||||||
|
|
||||||
peerConn.SetOnConnected(e.rpManager.OnConnected)
|
peerConn.SetOnConnected(e.rpManager.OnConnected)
|
||||||
peerConn.SetOnDisconnected(e.rpManager.OnDisconnected)
|
peerConn.SetOnDisconnected(e.rpManager.OnDisconnected)
|
||||||
}
|
}
|
||||||
@@ -976,7 +978,7 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, e
|
|||||||
func (e *Engine) receiveSignalEvents() {
|
func (e *Engine) receiveSignalEvents() {
|
||||||
go func() {
|
go func() {
|
||||||
// connect to a stream of messages coming from the signal server
|
// connect to a stream of messages coming from the signal server
|
||||||
err := e.signal.Receive(func(msg *sProto.Message) error {
|
err := e.signal.Receive(e.ctx, func(msg *sProto.Message) error {
|
||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
@@ -992,8 +994,6 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.RegisterProtoSupportMeta(msg.Body.GetFeaturesSupported())
|
|
||||||
|
|
||||||
var rosenpassPubKey []byte
|
var rosenpassPubKey []byte
|
||||||
rosenpassAddr := ""
|
rosenpassAddr := ""
|
||||||
if msg.GetBody().GetRosenpassConfig() != nil {
|
if msg.GetBody().GetRosenpassConfig() != nil {
|
||||||
@@ -1009,6 +1009,7 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
Version: msg.GetBody().GetNetBirdVersion(),
|
Version: msg.GetBody().GetNetBirdVersion(),
|
||||||
RosenpassPubKey: rosenpassPubKey,
|
RosenpassPubKey: rosenpassPubKey,
|
||||||
RosenpassAddr: rosenpassAddr,
|
RosenpassAddr: rosenpassAddr,
|
||||||
|
RelaySrvAddress: msg.GetBody().GetRelayServerAddress(),
|
||||||
})
|
})
|
||||||
case sProto.Body_ANSWER:
|
case sProto.Body_ANSWER:
|
||||||
remoteCred, err := signal.UnMarshalCredential(msg)
|
remoteCred, err := signal.UnMarshalCredential(msg)
|
||||||
@@ -1016,8 +1017,6 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.RegisterProtoSupportMeta(msg.GetBody().GetFeaturesSupported())
|
|
||||||
|
|
||||||
var rosenpassPubKey []byte
|
var rosenpassPubKey []byte
|
||||||
rosenpassAddr := ""
|
rosenpassAddr := ""
|
||||||
if msg.GetBody().GetRosenpassConfig() != nil {
|
if msg.GetBody().GetRosenpassConfig() != nil {
|
||||||
@@ -1033,6 +1032,7 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
Version: msg.GetBody().GetNetBirdVersion(),
|
Version: msg.GetBody().GetNetBirdVersion(),
|
||||||
RosenpassPubKey: rosenpassPubKey,
|
RosenpassPubKey: rosenpassPubKey,
|
||||||
RosenpassAddr: rosenpassAddr,
|
RosenpassAddr: rosenpassAddr,
|
||||||
|
RelaySrvAddress: msg.GetBody().GetRelayServerAddress(),
|
||||||
})
|
})
|
||||||
case sProto.Body_CANDIDATE:
|
case sProto.Body_CANDIDATE:
|
||||||
candidate, err := ice.UnmarshalCandidate(msg.GetBody().Payload)
|
candidate, err := ice.UnmarshalCandidate(msg.GetBody().Payload)
|
||||||
@@ -1040,7 +1040,8 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
log.Errorf("failed on parsing remote candidate %s -> %s", candidate, err)
|
log.Errorf("failed on parsing remote candidate %s -> %s", candidate, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn.OnRemoteCandidate(candidate)
|
|
||||||
|
go conn.OnRemoteCandidate(candidate, e.GetClientRoutes())
|
||||||
case sProto.Body_MODE:
|
case sProto.Body_MODE:
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1050,7 +1051,7 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
// happens if signal is unavailable for a long time.
|
// happens if signal is unavailable for a long time.
|
||||||
// We want to cancel the operation of the whole client
|
// We want to cancel the operation of the whole client
|
||||||
_ = CtxGetState(e.ctx).Wrap(ErrResetConnection)
|
_ = CtxGetState(e.ctx).Wrap(ErrResetConnection)
|
||||||
e.cancel()
|
e.clientCancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -1111,13 +1112,16 @@ func (e *Engine) parseNATExternalIPMappings() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) close() {
|
func (e *Engine) close() {
|
||||||
if err := e.wgProxyFactory.Free(); err != nil {
|
if e.wgProxyFactory != nil {
|
||||||
log.Errorf("failed closing ebpf proxy: %s", err)
|
if err := e.wgProxyFactory.Free(); err != nil {
|
||||||
|
log.Errorf("failed closing ebpf proxy: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop/restore DNS first so dbus and friends don't complain because of a missing interface
|
// stop/restore DNS first so dbus and friends don't complain because of a missing interface
|
||||||
if e.dnsServer != nil {
|
if e.dnsServer != nil {
|
||||||
e.dnsServer.Stop()
|
e.dnsServer.Stop()
|
||||||
|
e.dnsServer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.routeManager != nil {
|
if e.routeManager != nil {
|
||||||
@@ -1151,7 +1155,8 @@ func (e *Engine) close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) {
|
func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) {
|
||||||
netMap, err := e.mgmClient.GetNetworkMap()
|
info := system.GetInfo(e.ctx)
|
||||||
|
netMap, err := e.mgmClient.GetNetworkMap(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -1180,7 +1185,7 @@ func (e *Engine) newWgIface() (*iface.WGIface, error) {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
return iface.NewWGIFace(e.config.WgIfaceName, e.config.WgAddr, e.config.WgPort, e.config.WgPrivateKey.String(), iface.DefaultMTU, transportNet, mArgs)
|
return iface.NewWGIFace(e.config.WgIfaceName, e.config.WgAddr, e.config.WgPort, e.config.WgPrivateKey.String(), iface.DefaultMTU, transportNet, mArgs, e.addrViaRoutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) wgInterfaceCreate() (err error) {
|
func (e *Engine) wgInterfaceCreate() (err error) {
|
||||||
@@ -1229,6 +1234,31 @@ 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
|
||||||
|
func (e *Engine) GetRouteManager() routemanager.Manager {
|
||||||
|
return e.routeManager
|
||||||
|
}
|
||||||
|
|
||||||
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
|
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
|
||||||
iface, err := net.InterfaceByName(ifaceName)
|
iface, err := net.InterfaceByName(ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1307,7 +1337,7 @@ func (e *Engine) receiveProbeEvents() {
|
|||||||
|
|
||||||
for _, peer := range e.peerConns {
|
for _, peer := range e.peerConns {
|
||||||
key := peer.GetKey()
|
key := peer.GetKey()
|
||||||
wgStats, err := peer.GetConf().WgConfig.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)
|
||||||
}
|
}
|
||||||
@@ -1329,3 +1359,72 @@ func (e *Engine) probeSTUNs() []relay.ProbeResult {
|
|||||||
func (e *Engine) probeTURNs() []relay.ProbeResult {
|
func (e *Engine) probeTURNs() []relay.ProbeResult {
|
||||||
return relay.ProbeAll(e.ctx, relay.ProbeTURN, e.TURNs)
|
return relay.ProbeAll(e.ctx, relay.ProbeTURN, e.TURNs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Engine) restartEngine() {
|
||||||
|
if err := e.Stop(); err != nil {
|
||||||
|
log.Errorf("Failed to stop engine: %v", err)
|
||||||
|
}
|
||||||
|
if err := e.Start(); err != nil {
|
||||||
|
log.Errorf("Failed to start engine: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) startNetworkMonitor() {
|
||||||
|
if !e.config.NetworkMonitor {
|
||||||
|
log.Infof("Network monitor is disabled, not starting")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.networkMonitor = networkmonitor.New()
|
||||||
|
go func() {
|
||||||
|
var mu sync.Mutex
|
||||||
|
var debounceTimer *time.Timer
|
||||||
|
|
||||||
|
// Start the network monitor with a callback, Start will block until the monitor is stopped,
|
||||||
|
// a network change is detected, or an error occurs on start up
|
||||||
|
err := e.networkMonitor.Start(e.ctx, func() {
|
||||||
|
// This function is called when a network change is detected
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
if debounceTimer != nil {
|
||||||
|
debounceTimer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a new timer to debounce rapid network changes
|
||||||
|
debounceTimer = time.AfterFunc(1*time.Second, func() {
|
||||||
|
// This function is called after the debounce period
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
log.Infof("Network monitor detected network change, restarting engine")
|
||||||
|
e.restartEngine()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, networkmonitor.ErrStopped) {
|
||||||
|
log.Errorf("Network monitor: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) addrViaRoutes(addr netip.Addr) (bool, netip.Prefix, error) {
|
||||||
|
var vpnRoutes []netip.Prefix
|
||||||
|
for _, routes := range e.GetClientRoutes() {
|
||||||
|
if len(routes) > 0 && routes[0] != nil {
|
||||||
|
vpnRoutes = append(vpnRoutes, routes[0].Network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isVpn, prefix := systemops.IsAddrRouted(addr, vpnRoutes); isVpn {
|
||||||
|
return true, prefix, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, netip.Prefix{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isChecksEqual checks if two slices of checks are equal.
|
||||||
|
func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool {
|
||||||
|
return slices.EqualFunc(checks, oChecks, func(checks, oChecks *mgmProto.Checks) bool {
|
||||||
|
return slices.Equal(checks.Files, oChecks.Files)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
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"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/keepalive"
|
"google.golang.org/grpc/keepalive"
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/dns"
|
"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/routemanager"
|
"github.com/netbirdio/netbird/client/internal/routemanager"
|
||||||
@@ -34,6 +36,7 @@ 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"
|
||||||
|
relayClient "github.com/netbirdio/netbird/relay/client"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
signal "github.com/netbirdio/netbird/signal/client"
|
signal "github.com/netbirdio/netbird/signal/client"
|
||||||
"github.com/netbirdio/netbird/signal/proto"
|
"github.com/netbirdio/netbird/signal/proto"
|
||||||
@@ -55,10 +58,16 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEngine_SSH(t *testing.T) {
|
func TestMain(m *testing.M) {
|
||||||
|
_ = util.InitLog("debug", "console")
|
||||||
|
code := m.Run()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
func TestEngine_SSH(t *testing.T) {
|
||||||
t.Skip("skipping TestEngine_SSH on Windows")
|
// todo resolve test execution on freebsd
|
||||||
|
if runtime.GOOS == "windows" || runtime.GOOS == "freebsd" {
|
||||||
|
t.Skip("skipping TestEngine_SSH")
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := wgtypes.GeneratePrivateKey()
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
@@ -70,13 +79,23 @@ func TestEngine_SSH(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
relayMgr := relayClient.NewManager(ctx, "", key.PublicKey().String())
|
||||||
WgIfaceName: "utun101",
|
engine := NewEngine(
|
||||||
WgAddr: "100.64.0.1/24",
|
ctx, cancel,
|
||||||
WgPrivateKey: key,
|
&signal.MockClient{},
|
||||||
WgPort: 33100,
|
&mgmt.MockClient{},
|
||||||
ServerSSHAllowed: true,
|
relayMgr,
|
||||||
}, MobileDependency{}, peer.NewRecorder("https://mgm"))
|
&EngineConfig{
|
||||||
|
WgIfaceName: "utun101",
|
||||||
|
WgAddr: "100.64.0.1/24",
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: 33100,
|
||||||
|
ServerSSHAllowed: true,
|
||||||
|
},
|
||||||
|
MobileDependency{},
|
||||||
|
peer.NewRecorder("https://mgm"),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
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 },
|
||||||
@@ -172,7 +191,7 @@ func TestEngine_SSH(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//time.Sleep(250 * time.Millisecond)
|
// time.Sleep(250 * time.Millisecond)
|
||||||
assert.NotNil(t, engine.sshServer)
|
assert.NotNil(t, engine.sshServer)
|
||||||
assert.Contains(t, sshPeersRemoved, "MNHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=")
|
assert.Contains(t, sshPeersRemoved, "MNHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=")
|
||||||
|
|
||||||
@@ -205,21 +224,29 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
relayMgr := relayClient.NewManager(ctx, "", key.PublicKey().String())
|
||||||
WgIfaceName: "utun102",
|
engine := NewEngine(
|
||||||
WgAddr: "100.64.0.1/24",
|
ctx, cancel,
|
||||||
WgPrivateKey: key,
|
&signal.MockClient{},
|
||||||
WgPort: 33100,
|
&mgmt.MockClient{},
|
||||||
}, MobileDependency{}, peer.NewRecorder("https://mgm"))
|
relayMgr,
|
||||||
newNet, err := stdnet.NewNet()
|
&EngineConfig{
|
||||||
if err != nil {
|
WgIfaceName: "utun102",
|
||||||
t.Fatal(err)
|
WgAddr: "100.64.0.1/24",
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: 33100,
|
||||||
|
},
|
||||||
|
MobileDependency{},
|
||||||
|
peer.NewRecorder("https://mgm"),
|
||||||
|
nil)
|
||||||
|
|
||||||
|
wgIface := &iface.MockWGIface{
|
||||||
|
RemovePeerFunc: func(peerKey string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
engine.wgInterface, err = iface.NewWGIFace("utun102", "100.64.0.1/24", engine.config.WgPort, key.String(), iface.DefaultMTU, newNet, nil)
|
engine.wgInterface = wgIface
|
||||||
if err != nil {
|
engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), time.Minute, engine.wgInterface, engine.statusRecorder, nil)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), engine.wgInterface, engine.statusRecorder, nil)
|
|
||||||
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 },
|
||||||
}
|
}
|
||||||
@@ -228,6 +255,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
engine.udpMux = bind.NewUniversalUDPMuxDefault(bind.UniversalUDPMuxParams{UDPConn: conn})
|
engine.udpMux = bind.NewUniversalUDPMuxDefault(bind.UniversalUDPMuxParams{UDPConn: conn})
|
||||||
|
engine.ctx = ctx
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
@@ -391,7 +419,7 @@ func TestEngine_Sync(t *testing.T) {
|
|||||||
// feed updates to Engine via mocked Management client
|
// feed updates to Engine via mocked Management client
|
||||||
updates := make(chan *mgmtProto.SyncResponse)
|
updates := make(chan *mgmtProto.SyncResponse)
|
||||||
defer close(updates)
|
defer close(updates)
|
||||||
syncFunc := func(msgHandler func(msg *mgmtProto.SyncResponse) error) error {
|
syncFunc := func(ctx context.Context, info *system.Info, msgHandler func(msg *mgmtProto.SyncResponse) error) error {
|
||||||
for msg := range updates {
|
for msg := range updates {
|
||||||
err := msgHandler(msg)
|
err := msgHandler(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -400,13 +428,14 @@ func TestEngine_Sync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
relayMgr := relayClient.NewManager(ctx, "", key.PublicKey().String())
|
||||||
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{SyncFunc: syncFunc}, &EngineConfig{
|
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{SyncFunc: syncFunc}, relayMgr, &EngineConfig{
|
||||||
WgIfaceName: "utun103",
|
WgIfaceName: "utun103",
|
||||||
WgAddr: "100.64.0.1/24",
|
WgAddr: "100.64.0.1/24",
|
||||||
WgPrivateKey: key,
|
WgPrivateKey: key,
|
||||||
WgPort: 33100,
|
WgPort: 33100,
|
||||||
}, MobileDependency{}, peer.NewRecorder("https://mgm"))
|
}, MobileDependency{}, peer.NewRecorder("https://mgm"), nil)
|
||||||
|
engine.ctx = ctx
|
||||||
|
|
||||||
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 },
|
||||||
@@ -559,17 +588,19 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
|
|||||||
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
||||||
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
|
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
|
||||||
|
|
||||||
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
relayMgr := relayClient.NewManager(ctx, "", key.PublicKey().String())
|
||||||
|
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, relayMgr, &EngineConfig{
|
||||||
WgIfaceName: wgIfaceName,
|
WgIfaceName: wgIfaceName,
|
||||||
WgAddr: wgAddr,
|
WgAddr: wgAddr,
|
||||||
WgPrivateKey: key,
|
WgPrivateKey: key,
|
||||||
WgPort: 33100,
|
WgPort: 33100,
|
||||||
}, MobileDependency{}, peer.NewRecorder("https://mgm"))
|
}, MobileDependency{}, peer.NewRecorder("https://mgm"), nil)
|
||||||
|
engine.ctx = ctx
|
||||||
newNet, err := stdnet.NewNet()
|
newNet, err := stdnet.NewNet()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, engine.config.WgPort, key.String(), iface.DefaultMTU, newNet, nil)
|
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, engine.config.WgPort, key.String(), iface.DefaultMTU, newNet, nil, nil)
|
||||||
assert.NoError(t, err, "shouldn't return error")
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
input := struct {
|
input := struct {
|
||||||
inputSerial uint64
|
inputSerial uint64
|
||||||
@@ -577,10 +608,10 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
|
|||||||
}{}
|
}{}
|
||||||
|
|
||||||
mockRouteManager := &routemanager.MockManager{
|
mockRouteManager := &routemanager.MockManager{
|
||||||
UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) error {
|
UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, error) {
|
||||||
input.inputSerial = updateSerial
|
input.inputSerial = updateSerial
|
||||||
input.inputRoutes = newRoutes
|
input.inputRoutes = newRoutes
|
||||||
return testCase.inputErr
|
return nil, nil, testCase.inputErr
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,8 +628,8 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
|
|||||||
err = engine.updateNetworkMap(testCase.networkMap)
|
err = engine.updateNetworkMap(testCase.networkMap)
|
||||||
assert.NoError(t, err, "shouldn't return error")
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
||||||
assert.Len(t, input.inputRoutes, testCase.expectedLen, "routes len should match")
|
assert.Len(t, input.inputRoutes, testCase.expectedLen, "clientRoutes len should match")
|
||||||
assert.Equal(t, testCase.expectedRoutes, input.inputRoutes, "routes should match")
|
assert.Equal(t, testCase.expectedRoutes, input.inputRoutes, "clientRoutes should match")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -728,22 +759,25 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
|
|||||||
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
||||||
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
|
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
|
||||||
|
|
||||||
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
relayMgr := relayClient.NewManager(ctx, "", key.PublicKey().String())
|
||||||
|
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, relayMgr, &EngineConfig{
|
||||||
WgIfaceName: wgIfaceName,
|
WgIfaceName: wgIfaceName,
|
||||||
WgAddr: wgAddr,
|
WgAddr: wgAddr,
|
||||||
WgPrivateKey: key,
|
WgPrivateKey: key,
|
||||||
WgPort: 33100,
|
WgPort: 33100,
|
||||||
}, MobileDependency{}, peer.NewRecorder("https://mgm"))
|
}, MobileDependency{}, peer.NewRecorder("https://mgm"), nil)
|
||||||
|
engine.ctx = ctx
|
||||||
|
|
||||||
newNet, err := stdnet.NewNet()
|
newNet, err := stdnet.NewNet()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, 33100, key.String(), iface.DefaultMTU, newNet, nil)
|
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, 33100, key.String(), iface.DefaultMTU, newNet, nil, nil)
|
||||||
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) error {
|
UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, error) {
|
||||||
return nil
|
return nil, nil, nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,13 +838,13 @@ func TestEngine_MultiplePeers(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
sigServer, signalAddr, err := startSignal()
|
sigServer, signalAddr, err := startSignal(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer sigServer.Stop()
|
defer sigServer.Stop()
|
||||||
mgmtServer, mgmtAddr, err := startManagement(dir)
|
mgmtServer, mgmtAddr, err := startManagement(t, dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
return
|
return
|
||||||
@@ -1002,10 +1036,15 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin
|
|||||||
WgPort: wgPort,
|
WgPort: wgPort,
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewEngine(ctx, cancel, signalClient, mgmtClient, conf, MobileDependency{}, peer.NewRecorder("https://mgm")), nil
|
relayMgr := relayClient.NewManager(ctx, "", key.PublicKey().String())
|
||||||
|
e, err := NewEngine(ctx, cancel, signalClient, mgmtClient, relayMgr, conf, MobileDependency{}, peer.NewRecorder("https://mgm"), nil), nil
|
||||||
|
e.ctx = ctx
|
||||||
|
return e, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func startSignal() (*grpc.Server, string, error) {
|
func startSignal(t *testing.T) (*grpc.Server, string, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
||||||
|
|
||||||
lis, err := net.Listen("tcp", "localhost:0")
|
lis, err := net.Listen("tcp", "localhost:0")
|
||||||
@@ -1013,7 +1052,9 @@ func startSignal() (*grpc.Server, string, error) {
|
|||||||
log.Fatalf("failed to listen: %v", err)
|
log.Fatalf("failed to listen: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
proto.RegisterSignalExchangeServer(s, signalServer.NewServer())
|
srv, err := signalServer.NewServer(otel.Meter(""))
|
||||||
|
require.NoError(t, err)
|
||||||
|
proto.RegisterSignalExchangeServer(s, srv)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err = s.Serve(lis); err != nil {
|
if err = s.Serve(lis); err != nil {
|
||||||
@@ -1024,7 +1065,9 @@ func startSignal() (*grpc.Server, string, error) {
|
|||||||
return s, lis.Addr().String(), nil
|
return s, lis.Addr().String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startManagement(dataDir string) (*grpc.Server, string, error) {
|
func startManagement(t *testing.T, dataDir string) (*grpc.Server, string, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
config := &server.Config{
|
config := &server.Config{
|
||||||
Stuns: []*server.Host{},
|
Stuns: []*server.Host{},
|
||||||
TURNConfig: &server.TURNConfig{},
|
TURNConfig: &server.TURNConfig{},
|
||||||
@@ -1041,23 +1084,25 @@ func startManagement(dataDir string) (*grpc.Server, string, error) {
|
|||||||
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, err := server.NewStoreFromJson(config.Datadir, nil)
|
|
||||||
|
store, cleanUp, err := server.NewTestStoreFromJson(context.Background(), config.Datadir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
t.Cleanup(cleanUp)
|
||||||
|
|
||||||
peersUpdateManager := server.NewPeersUpdateManager(nil)
|
peersUpdateManager := server.NewPeersUpdateManager(nil)
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
ia, _ := integrations.NewIntegratedValidator(eventStore)
|
ia, _ := integrations.NewIntegratedValidator(context.Background(), eventStore)
|
||||||
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia)
|
accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
|
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, "")
|
||||||
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, nil, nil)
|
mgmtServer, err := server.NewServer(context.Background(), config, accountManager, peersUpdateManager, turnManager, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func Login(ctx context.Context, config *Config, setupKey string, jwtToken string
|
|||||||
}
|
}
|
||||||
|
|
||||||
serverKey, err := doMgmLogin(ctx, mgmClient, pubSSHKey)
|
serverKey, err := doMgmLogin(ctx, mgmClient, pubSSHKey)
|
||||||
if isRegistrationNeeded(err) {
|
if serverKey != nil && isRegistrationNeeded(err) {
|
||||||
log.Debugf("peer registration required")
|
log.Debugf("peer registration required")
|
||||||
_, err = registerPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey)
|
_, err = registerPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey)
|
||||||
return err
|
return err
|
||||||
|
|||||||
21
client/internal/networkmonitor/monitor.go
Normal file
21
client/internal/networkmonitor/monitor.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package networkmonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrStopped = errors.New("monitor has been stopped")
|
||||||
|
|
||||||
|
// NetworkMonitor watches for changes in network configuration.
|
||||||
|
type NetworkMonitor struct {
|
||||||
|
cancel context.CancelFunc
|
||||||
|
wg sync.WaitGroup
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new network monitor.
|
||||||
|
func New() *NetworkMonitor {
|
||||||
|
return &NetworkMonitor{}
|
||||||
|
}
|
||||||
95
client/internal/networkmonitor/monitor_bsd.go
Normal file
95
client/internal/networkmonitor/monitor_bsd.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
//go:build (darwin && !ios) || dragonfly || freebsd || netbsd || openbsd
|
||||||
|
|
||||||
|
package networkmonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/net/route"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop, callback func()) error {
|
||||||
|
fd, err := unix.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open routing socket: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := unix.Close(fd); err != nil {
|
||||||
|
log.Errorf("Network monitor: failed to close routing socket: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ErrStopped
|
||||||
|
default:
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
n, err := unix.Read(fd, buf)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Network monitor: failed to read from routing socket: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n < unix.SizeofRtMsghdr {
|
||||||
|
log.Errorf("Network monitor: read from routing socket returned less than expected: %d bytes", n)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := (*unix.RtMsghdr)(unsafe.Pointer(&buf[0]))
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
// handle route changes
|
||||||
|
case unix.RTM_ADD, syscall.RTM_DELETE:
|
||||||
|
route, err := parseRouteMessage(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Network monitor: error parsing routing message: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !route.Dst.Addr().IsUnspecified() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
intf := "<nil>"
|
||||||
|
if route.Interface != nil {
|
||||||
|
intf = route.Interface.Name
|
||||||
|
}
|
||||||
|
switch msg.Type {
|
||||||
|
case unix.RTM_ADD:
|
||||||
|
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
|
||||||
|
go callback()
|
||||||
|
case unix.RTM_DELETE:
|
||||||
|
if nexthopv4.Intf != nil && route.Gw.Compare(nexthopv4.IP) == 0 || nexthopv6.Intf != nil && route.Gw.Compare(nexthopv6.IP) == 0 {
|
||||||
|
log.Infof("Network monitor: default route removed: via %s, interface %s", route.Gw, intf)
|
||||||
|
go callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRouteMessage(buf []byte) (*systemops.Route, error) {
|
||||||
|
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse RIB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msgs) != 1 {
|
||||||
|
return nil, fmt.Errorf("unexpected RIB message msgs: %v", msgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, ok := msgs[0].(*route.RouteMessage)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected RIB message type: %T", msgs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemops.MsgToRoute(msg)
|
||||||
|
}
|
||||||
82
client/internal/networkmonitor/monitor_generic.go
Normal file
82
client/internal/networkmonitor/monitor_generic.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
//go:build !ios && !android
|
||||||
|
|
||||||
|
package networkmonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start begins monitoring network changes. When a change is detected, it calls the callback asynchronously and returns.
|
||||||
|
func (nw *NetworkMonitor) Start(ctx context.Context, callback func()) (err error) {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
nw.mu.Lock()
|
||||||
|
ctx, nw.cancel = context.WithCancel(ctx)
|
||||||
|
nw.mu.Unlock()
|
||||||
|
|
||||||
|
nw.wg.Add(1)
|
||||||
|
defer nw.wg.Done()
|
||||||
|
|
||||||
|
var nexthop4, nexthop6 systemops.Nexthop
|
||||||
|
|
||||||
|
operation := func() error {
|
||||||
|
var errv4, errv6 error
|
||||||
|
nexthop4, errv4 = systemops.GetNextHop(netip.IPv4Unspecified())
|
||||||
|
nexthop6, errv6 = systemops.GetNextHop(netip.IPv6Unspecified())
|
||||||
|
|
||||||
|
if errv4 != nil && errv6 != nil {
|
||||||
|
return errors.New("failed to get default next hops")
|
||||||
|
}
|
||||||
|
|
||||||
|
if errv4 == nil {
|
||||||
|
log.Debugf("Network monitor: IPv4 default route: %s, interface: %s", nexthop4.IP, nexthop4.Intf.Name)
|
||||||
|
}
|
||||||
|
if errv6 == nil {
|
||||||
|
log.Debugf("Network monitor: IPv6 default route: %s, interface: %s", nexthop6.IP, nexthop6.Intf.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// continue if either route was found
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expBackOff := backoff.WithContext(backoff.NewExponentialBackOff(), ctx)
|
||||||
|
|
||||||
|
if err := backoff.Retry(operation, expBackOff); err != nil {
|
||||||
|
return fmt.Errorf("failed to get default next hops: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recover in case sys ops panic
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("panic occurred: %v, stack trace: %s", r, string(debug.Stack()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := checkChange(ctx, nexthop4, nexthop6, callback); err != nil {
|
||||||
|
return fmt.Errorf("check change: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the network monitor.
|
||||||
|
func (nw *NetworkMonitor) Stop() {
|
||||||
|
nw.mu.Lock()
|
||||||
|
defer nw.mu.Unlock()
|
||||||
|
|
||||||
|
if nw.cancel != nil {
|
||||||
|
nw.cancel()
|
||||||
|
nw.wg.Wait()
|
||||||
|
}
|
||||||
|
}
|
||||||
57
client/internal/networkmonitor/monitor_linux.go
Normal file
57
client/internal/networkmonitor/monitor_linux.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
//go:build !android
|
||||||
|
|
||||||
|
package networkmonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop, callback func()) error {
|
||||||
|
if nexthopv4.Intf == nil && nexthopv6.Intf == nil {
|
||||||
|
return errors.New("no interfaces available")
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
routeChan := make(chan netlink.RouteUpdate)
|
||||||
|
if err := netlink.RouteSubscribe(routeChan, done); err != nil {
|
||||||
|
return fmt.Errorf("subscribe to route updates: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Network monitor: started")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ErrStopped
|
||||||
|
|
||||||
|
// handle route changes
|
||||||
|
case route := <-routeChan:
|
||||||
|
// default route and main table
|
||||||
|
if route.Dst != nil || route.Table != syscall.RT_TABLE_MAIN {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch route.Type {
|
||||||
|
// triggered on added/replaced routes
|
||||||
|
case syscall.RTM_NEWROUTE:
|
||||||
|
log.Infof("Network monitor: default route changed: via %s, interface %d", route.Gw, route.LinkIndex)
|
||||||
|
go callback()
|
||||||
|
return nil
|
||||||
|
case syscall.RTM_DELROUTE:
|
||||||
|
if nexthopv4.Intf != nil && route.Gw.Equal(nexthopv4.IP.AsSlice()) || nexthopv6.Intf != nil && route.Gw.Equal(nexthopv6.IP.AsSlice()) {
|
||||||
|
log.Infof("Network monitor: default route removed: via %s, interface %d", route.Gw, route.LinkIndex)
|
||||||
|
go callback()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
client/internal/networkmonitor/monitor_mobile.go
Normal file
12
client/internal/networkmonitor/monitor_mobile.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build ios || android
|
||||||
|
|
||||||
|
package networkmonitor
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
func (nw *NetworkMonitor) Start(context.Context, func()) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nw *NetworkMonitor) Stop() {
|
||||||
|
}
|
||||||
245
client/internal/networkmonitor/monitor_windows.go
Normal file
245
client/internal/networkmonitor/monitor_windows.go
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
package networkmonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
unreachable = 0
|
||||||
|
incomplete = 1
|
||||||
|
probe = 2
|
||||||
|
delay = 3
|
||||||
|
stale = 4
|
||||||
|
reachable = 5
|
||||||
|
permanent = 6
|
||||||
|
tbd = 7
|
||||||
|
)
|
||||||
|
|
||||||
|
const interval = 10 * time.Second
|
||||||
|
|
||||||
|
func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop, callback func()) error {
|
||||||
|
var neighborv4, neighborv6 *systemops.Neighbor
|
||||||
|
{
|
||||||
|
initialNeighbors, err := getNeighbors()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get neighbors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
neighborv4 = assignNeighbor(nexthopv4, initialNeighbors)
|
||||||
|
neighborv6 = assignNeighbor(nexthopv6, initialNeighbors)
|
||||||
|
}
|
||||||
|
log.Debugf("Network monitor: initial IPv4 neighbor: %v, IPv6 neighbor: %v", neighborv4, neighborv6)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ErrStopped
|
||||||
|
case <-ticker.C:
|
||||||
|
if changed(nexthopv4, neighborv4, nexthopv6, neighborv6) {
|
||||||
|
go callback()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assignNeighbor(nexthop systemops.Nexthop, initialNeighbors map[netip.Addr]systemops.Neighbor) *systemops.Neighbor {
|
||||||
|
if n, ok := initialNeighbors[nexthop.IP]; ok &&
|
||||||
|
n.State != unreachable &&
|
||||||
|
n.State != incomplete &&
|
||||||
|
n.State != tbd {
|
||||||
|
return &n
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func changed(
|
||||||
|
nexthopv4 systemops.Nexthop,
|
||||||
|
neighborv4 *systemops.Neighbor,
|
||||||
|
nexthopv6 systemops.Nexthop,
|
||||||
|
neighborv6 *systemops.Neighbor,
|
||||||
|
) bool {
|
||||||
|
neighbors, err := getNeighbors()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("network monitor: error fetching current neighbors: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if neighborChanged(nexthopv4, neighborv4, neighbors) || neighborChanged(nexthopv6, neighborv6, neighbors) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, err := getRoutes()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("network monitor: error fetching current routes: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if routeChanged(nexthopv4, nexthopv4.Intf, routes) || routeChanged(nexthopv6, nexthopv6.Intf, routes) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// routeChanged checks if the default routes still point to our nexthop/interface
|
||||||
|
func routeChanged(nexthop systemops.Nexthop, intf *net.Interface, routes []systemops.Route) bool {
|
||||||
|
if !nexthop.IP.IsValid() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
unspec := getUnspecifiedPrefix(nexthop.IP)
|
||||||
|
defaultRoutes, foundMatchingRoute := processRoutes(nexthop, intf, routes, unspec)
|
||||||
|
|
||||||
|
log.Tracef("network monitor: all default routes:\n%s", strings.Join(defaultRoutes, "\n"))
|
||||||
|
|
||||||
|
if !foundMatchingRoute {
|
||||||
|
logRouteChange(nexthop.IP, intf)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUnspecifiedPrefix(ip netip.Addr) netip.Prefix {
|
||||||
|
if ip.Is6() {
|
||||||
|
return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
|
||||||
|
}
|
||||||
|
return netip.PrefixFrom(netip.IPv4Unspecified(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processRoutes(nexthop systemops.Nexthop, intf *net.Interface, routes []systemops.Route, unspec netip.Prefix) ([]string, bool) {
|
||||||
|
var defaultRoutes []string
|
||||||
|
foundMatchingRoute := false
|
||||||
|
|
||||||
|
for _, r := range routes {
|
||||||
|
if r.Destination == unspec {
|
||||||
|
routeInfo := formatRouteInfo(r)
|
||||||
|
defaultRoutes = append(defaultRoutes, routeInfo)
|
||||||
|
|
||||||
|
if r.Nexthop == nexthop.IP && compareIntf(r.Interface, intf) == 0 {
|
||||||
|
foundMatchingRoute = true
|
||||||
|
log.Debugf("network monitor: found matching default route: %s", routeInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultRoutes, foundMatchingRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatRouteInfo(r systemops.Route) string {
|
||||||
|
newIntf := "<nil>"
|
||||||
|
if r.Interface != nil {
|
||||||
|
newIntf = r.Interface.Name
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Nexthop: %s, Interface: %s", r.Nexthop, newIntf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logRouteChange(ip netip.Addr, intf *net.Interface) {
|
||||||
|
oldIntf := "<nil>"
|
||||||
|
if intf != nil {
|
||||||
|
oldIntf = intf.Name
|
||||||
|
}
|
||||||
|
log.Infof("network monitor: default route for %s (%s) is gone or changed", ip, oldIntf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func neighborChanged(nexthop systemops.Nexthop, neighbor *systemops.Neighbor, neighbors map[netip.Addr]systemops.Neighbor) bool {
|
||||||
|
if neighbor == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: consider non-local nexthops, e.g. on point-to-point interfaces
|
||||||
|
if n, ok := neighbors[nexthop.IP]; ok {
|
||||||
|
if n.State == unreachable || n.State == incomplete {
|
||||||
|
log.Infof("network monitor: neighbor %s (%s) is not reachable: %s", neighbor.IPAddress, neighbor.LinkLayerAddress, stateFromInt(n.State))
|
||||||
|
return true
|
||||||
|
} else if n.InterfaceIndex != neighbor.InterfaceIndex {
|
||||||
|
log.Infof(
|
||||||
|
"network monitor: neighbor %s (%s) changed interface from '%s' (%d) to '%s' (%d): %s",
|
||||||
|
neighbor.IPAddress,
|
||||||
|
neighbor.LinkLayerAddress,
|
||||||
|
neighbor.InterfaceAlias,
|
||||||
|
neighbor.InterfaceIndex,
|
||||||
|
n.InterfaceAlias,
|
||||||
|
n.InterfaceIndex,
|
||||||
|
stateFromInt(n.State),
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("network monitor: neighbor %s (%s) is gone", neighbor.IPAddress, neighbor.LinkLayerAddress)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNeighbors() (map[netip.Addr]systemops.Neighbor, error) {
|
||||||
|
entries, err := systemops.GetNeighbors()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get neighbors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
neighbours := make(map[netip.Addr]systemops.Neighbor, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
neighbours[entry.IPAddress] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
return neighbours, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRoutes() ([]systemops.Route, error) {
|
||||||
|
entries, err := systemops.GetRoutes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get routes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateFromInt(state uint8) string {
|
||||||
|
switch state {
|
||||||
|
case unreachable:
|
||||||
|
return "unreachable"
|
||||||
|
case incomplete:
|
||||||
|
return "incomplete"
|
||||||
|
case probe:
|
||||||
|
return "probe"
|
||||||
|
case delay:
|
||||||
|
return "delay"
|
||||||
|
case stale:
|
||||||
|
return "stale"
|
||||||
|
case reachable:
|
||||||
|
return "reachable"
|
||||||
|
case permanent:
|
||||||
|
return "permanent"
|
||||||
|
case tbd:
|
||||||
|
return "tbd"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareIntf(a, b *net.Interface) int {
|
||||||
|
if a == nil && b == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if a == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if b == nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return a.Index - b.Index
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,34 @@
|
|||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/magiconair/properties/assert"
|
"github.com/magiconair/properties/assert"
|
||||||
"github.com/pion/stun/v2"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/client/internal/wgproxy"
|
"github.com/netbirdio/netbird/client/internal/wgproxy"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var connConf = ConnConfig{
|
var connConf = ConnConfig{
|
||||||
Key: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
Key: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
LocalKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
LocalKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
StunTurn: []*stun.URI{},
|
Timeout: time.Second,
|
||||||
InterfaceBlackList: nil,
|
LocalWgPort: 51820,
|
||||||
Timeout: time.Second,
|
ICEConfig: ICEConfig{
|
||||||
LocalWgPort: 51820,
|
InterfaceBlackList: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
_ = util.InitLog("trace", "console")
|
||||||
|
code := m.Run()
|
||||||
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewConn_interfaceFilter(t *testing.T) {
|
func TestNewConn_interfaceFilter(t *testing.T) {
|
||||||
@@ -35,11 +44,11 @@ func TestNewConn_interfaceFilter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConn_GetKey(t *testing.T) {
|
func TestConn_GetKey(t *testing.T) {
|
||||||
wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort)
|
wgProxyFactory := wgproxy.NewFactory(context.Background(), false, connConf.LocalWgPort)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = wgProxyFactory.Free()
|
_ = wgProxyFactory.Free()
|
||||||
}()
|
}()
|
||||||
conn, err := NewConn(connConf, nil, wgProxyFactory, nil, nil)
|
conn, err := NewConn(context.Background(), connConf, nil, wgProxyFactory, nil, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -50,11 +59,11 @@ func TestConn_GetKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConn_OnRemoteOffer(t *testing.T) {
|
func TestConn_OnRemoteOffer(t *testing.T) {
|
||||||
wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort)
|
wgProxyFactory := wgproxy.NewFactory(context.Background(), false, connConf.LocalWgPort)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = wgProxyFactory.Free()
|
_ = wgProxyFactory.Free()
|
||||||
}()
|
}()
|
||||||
conn, err := NewConn(connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil)
|
conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -62,7 +71,7 @@ func TestConn_OnRemoteOffer(t *testing.T) {
|
|||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
go func() {
|
go func() {
|
||||||
<-conn.remoteOffersCh
|
<-conn.handshaker.remoteOffersCh
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -87,11 +96,11 @@ func TestConn_OnRemoteOffer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConn_OnRemoteAnswer(t *testing.T) {
|
func TestConn_OnRemoteAnswer(t *testing.T) {
|
||||||
wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort)
|
wgProxyFactory := wgproxy.NewFactory(context.Background(), false, connConf.LocalWgPort)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = wgProxyFactory.Free()
|
_ = wgProxyFactory.Free()
|
||||||
}()
|
}()
|
||||||
conn, err := NewConn(connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil)
|
conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -99,7 +108,7 @@ func TestConn_OnRemoteAnswer(t *testing.T) {
|
|||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
go func() {
|
go func() {
|
||||||
<-conn.remoteAnswerCh
|
<-conn.handshaker.remoteAnswerCh
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -123,62 +132,37 @@ func TestConn_OnRemoteAnswer(t *testing.T) {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
func TestConn_Status(t *testing.T) {
|
func TestConn_Status(t *testing.T) {
|
||||||
wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort)
|
wgProxyFactory := wgproxy.NewFactory(context.Background(), false, connConf.LocalWgPort)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = wgProxyFactory.Free()
|
_ = wgProxyFactory.Free()
|
||||||
}()
|
}()
|
||||||
conn, err := NewConn(connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil)
|
conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tables := []struct {
|
tables := []struct {
|
||||||
name string
|
name string
|
||||||
status ConnStatus
|
statusIce ConnStatus
|
||||||
want ConnStatus
|
statusRelay ConnStatus
|
||||||
|
want ConnStatus
|
||||||
}{
|
}{
|
||||||
{"StatusConnected", StatusConnected, StatusConnected},
|
{"StatusConnected", StatusConnected, StatusConnected, StatusConnected},
|
||||||
{"StatusDisconnected", StatusDisconnected, StatusDisconnected},
|
{"StatusDisconnected", StatusDisconnected, StatusDisconnected, StatusDisconnected},
|
||||||
{"StatusConnecting", StatusConnecting, StatusConnecting},
|
{"StatusConnecting", StatusConnecting, StatusConnecting, StatusConnecting},
|
||||||
|
{"StatusConnectingIce", StatusConnecting, StatusDisconnected, StatusConnecting},
|
||||||
|
{"StatusConnectingIceAlternative", StatusConnecting, StatusConnected, StatusConnected},
|
||||||
|
{"StatusConnectingRelay", StatusDisconnected, StatusConnecting, StatusConnecting},
|
||||||
|
{"StatusConnectingRelayAlternative", StatusConnected, StatusConnecting, StatusConnected},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
t.Run(table.name, func(t *testing.T) {
|
t.Run(table.name, func(t *testing.T) {
|
||||||
conn.status = table.status
|
conn.statusICE = table.statusIce
|
||||||
|
conn.statusRelay = table.statusRelay
|
||||||
|
|
||||||
got := conn.Status()
|
got := conn.Status()
|
||||||
assert.Equal(t, got, table.want, "they should be equal")
|
assert.Equal(t, got, table.want, "they should be equal")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConn_Close(t *testing.T) {
|
|
||||||
wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort)
|
|
||||||
defer func() {
|
|
||||||
_ = wgProxyFactory.Free()
|
|
||||||
}()
|
|
||||||
conn, err := NewConn(connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
<-conn.closeCh
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
err := conn.Close()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ const (
|
|||||||
envICEForceRelayConn = "NB_ICE_FORCE_RELAY_CONN"
|
envICEForceRelayConn = "NB_ICE_FORCE_RELAY_CONN"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func ForcedRelayAddress() string {
|
||||||
|
if envRelay := os.Getenv("NB_RELAY_ADDRESS"); envRelay != "" {
|
||||||
|
return envRelay
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func iceKeepAlive() time.Duration {
|
func iceKeepAlive() time.Duration {
|
||||||
keepAliveEnv := os.Getenv(envICEKeepAliveIntervalSec)
|
keepAliveEnv := os.Getenv(envICEKeepAliveIntervalSec)
|
||||||
if keepAliveEnv == "" {
|
if keepAliveEnv == "" {
|
||||||
|
|||||||
195
client/internal/peer/handshaker.go
Normal file
195
client/internal/peer/handshaker.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrSignalIsNotReady = errors.New("signal is not ready")
|
||||||
|
)
|
||||||
|
|
||||||
|
// IceCredentials ICE protocol credentials struct
|
||||||
|
type IceCredentials struct {
|
||||||
|
UFrag string
|
||||||
|
Pwd string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OfferAnswer represents a session establishment offer or answer
|
||||||
|
type OfferAnswer struct {
|
||||||
|
IceCredentials IceCredentials
|
||||||
|
// WgListenPort is a remote WireGuard listen port.
|
||||||
|
// This field is used when establishing a direct WireGuard connection without any proxy.
|
||||||
|
// We can set the remote peer's endpoint with this port.
|
||||||
|
WgListenPort int
|
||||||
|
|
||||||
|
// Version of NetBird Agent
|
||||||
|
Version string
|
||||||
|
// RosenpassPubKey is the Rosenpass public key of the remote peer when receiving this message
|
||||||
|
// This value is the local Rosenpass server public key when sending the message
|
||||||
|
RosenpassPubKey []byte
|
||||||
|
// RosenpassAddr is the Rosenpass server address (IP:port) of the remote peer when receiving this message
|
||||||
|
// This value is the local Rosenpass server address when sending the message
|
||||||
|
RosenpassAddr string
|
||||||
|
|
||||||
|
// relay server address
|
||||||
|
RelaySrvAddress string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HandshakeArgs struct {
|
||||||
|
IceUFrag string
|
||||||
|
IcePwd string
|
||||||
|
RelayAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handshaker struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
ctx context.Context
|
||||||
|
log *log.Entry
|
||||||
|
config ConnConfig
|
||||||
|
signaler *Signaler
|
||||||
|
onNewOfferListeners []func(*OfferAnswer)
|
||||||
|
|
||||||
|
// remoteOffersCh is a channel used to wait for remote credentials to proceed with the connection
|
||||||
|
remoteOffersCh chan OfferAnswer
|
||||||
|
// remoteAnswerCh is a channel used to wait for remote credentials answer (confirmation of our offer) to proceed with the connection
|
||||||
|
remoteAnswerCh chan OfferAnswer
|
||||||
|
|
||||||
|
lastOfferArgs HandshakeArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandshaker(ctx context.Context, log *log.Entry, config ConnConfig, signaler *Signaler) *Handshaker {
|
||||||
|
return &Handshaker{
|
||||||
|
ctx: ctx,
|
||||||
|
log: log,
|
||||||
|
config: config,
|
||||||
|
signaler: signaler,
|
||||||
|
remoteOffersCh: make(chan OfferAnswer),
|
||||||
|
remoteAnswerCh: make(chan OfferAnswer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handshaker) AddOnNewOfferListener(offer func(remoteOfferAnswer *OfferAnswer)) {
|
||||||
|
h.onNewOfferListeners = append(h.onNewOfferListeners, offer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handshaker) Listen() {
|
||||||
|
for {
|
||||||
|
h.log.Debugf("wait for remote offer confirmation")
|
||||||
|
remoteOfferAnswer, err := h.waitForRemoteOfferConfirmation()
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(*ConnectionClosedError); ok {
|
||||||
|
h.log.Tracef("stop handshaker")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.log.Errorf("failed to received remote offer confirmation: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
h.log.Debugf("received connection confirmation, running version %s and with remote WireGuard listen port %d", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort)
|
||||||
|
for _, listener := range h.onNewOfferListeners {
|
||||||
|
go listener(remoteOfferAnswer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handshaker) SendOffer(args HandshakeArgs) error {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
err := h.sendOffer(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.lastOfferArgs = args
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnRemoteOffer handles an offer from the remote peer and returns true if the message was accepted, false otherwise
|
||||||
|
// doesn't block, discards the message if connection wasn't ready
|
||||||
|
func (h *Handshaker) OnRemoteOffer(offer OfferAnswer) bool {
|
||||||
|
// todo remove this if signaling can support relay
|
||||||
|
if ForcedRelayAddress() != "" {
|
||||||
|
offer.RelaySrvAddress = ForcedRelayAddress()
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case h.remoteOffersCh <- offer:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
h.log.Debugf("OnRemoteOffer skipping message because is not ready")
|
||||||
|
// connection might not be ready yet to receive so we ignore the message
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnRemoteAnswer handles an offer from the remote peer and returns true if the message was accepted, false otherwise
|
||||||
|
// doesn't block, discards the message if connection wasn't ready
|
||||||
|
func (h *Handshaker) OnRemoteAnswer(answer OfferAnswer) bool {
|
||||||
|
// todo remove this if signaling can support relay
|
||||||
|
if ForcedRelayAddress() != "" {
|
||||||
|
answer.RelaySrvAddress = ForcedRelayAddress()
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case h.remoteAnswerCh <- answer:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
// connection might not be ready yet to receive so we ignore the message
|
||||||
|
h.log.Debugf("OnRemoteAnswer skipping message because is not ready")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handshaker) waitForRemoteOfferConfirmation() (*OfferAnswer, error) {
|
||||||
|
select {
|
||||||
|
case remoteOfferAnswer := <-h.remoteOffersCh:
|
||||||
|
// received confirmation from the remote peer -> ready to proceed
|
||||||
|
err := h.sendAnswer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &remoteOfferAnswer, nil
|
||||||
|
case remoteOfferAnswer := <-h.remoteAnswerCh:
|
||||||
|
return &remoteOfferAnswer, nil
|
||||||
|
case <-h.ctx.Done():
|
||||||
|
// closed externally
|
||||||
|
return nil, NewConnectionClosedError(h.config.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendOffer prepares local user credentials and signals them to the remote peer
|
||||||
|
func (h *Handshaker) sendOffer(args HandshakeArgs) error {
|
||||||
|
offer := OfferAnswer{
|
||||||
|
IceCredentials: IceCredentials{args.IceUFrag, args.IcePwd},
|
||||||
|
WgListenPort: h.config.LocalWgPort,
|
||||||
|
Version: version.NetbirdVersion(),
|
||||||
|
RosenpassPubKey: h.config.RosenpassPubKey,
|
||||||
|
RosenpassAddr: h.config.RosenpassAddr,
|
||||||
|
RelaySrvAddress: args.RelayAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.signaler.SignalOffer(offer, h.config.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handshaker) sendAnswer() error {
|
||||||
|
h.log.Debugf("sending answer")
|
||||||
|
answer := OfferAnswer{
|
||||||
|
IceCredentials: IceCredentials{h.lastOfferArgs.IceUFrag, h.lastOfferArgs.IcePwd},
|
||||||
|
WgListenPort: h.config.LocalWgPort,
|
||||||
|
Version: version.NetbirdVersion(),
|
||||||
|
RosenpassPubKey: h.config.RosenpassPubKey,
|
||||||
|
RosenpassAddr: h.config.RosenpassAddr,
|
||||||
|
RelaySrvAddress: h.lastOfferArgs.RelayAddr,
|
||||||
|
}
|
||||||
|
err := h.signaler.SignalAnswer(answer, h.config.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
70
client/internal/peer/signaler.go
Normal file
70
client/internal/peer/signaler.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pion/ice/v3"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
|
signal "github.com/netbirdio/netbird/signal/client"
|
||||||
|
sProto "github.com/netbirdio/netbird/signal/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Signaler struct {
|
||||||
|
signal signal.Client
|
||||||
|
wgPrivateKey wgtypes.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSignaler(signal signal.Client, wgPrivateKey wgtypes.Key) *Signaler {
|
||||||
|
return &Signaler{
|
||||||
|
signal: signal,
|
||||||
|
wgPrivateKey: wgPrivateKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Signaler) SignalOffer(offer OfferAnswer, remoteKey string) error {
|
||||||
|
return s.signalOfferAnswer(offer, remoteKey, sProto.Body_OFFER)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Signaler) SignalAnswer(offer OfferAnswer, remoteKey string) error {
|
||||||
|
return s.signalOfferAnswer(offer, remoteKey, sProto.Body_ANSWER)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Signaler) SignalICECandidate(candidate ice.Candidate, remoteKey string) error {
|
||||||
|
return s.signal.Send(&sProto.Message{
|
||||||
|
Key: s.wgPrivateKey.PublicKey().String(),
|
||||||
|
RemoteKey: remoteKey,
|
||||||
|
Body: &sProto.Body{
|
||||||
|
Type: sProto.Body_CANDIDATE,
|
||||||
|
Payload: candidate.Marshal(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Signaler) Ready() bool {
|
||||||
|
return s.signal.Ready()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignalOfferAnswer signals either an offer or an answer to remote peer
|
||||||
|
func (s *Signaler) signalOfferAnswer(offerAnswer OfferAnswer, remoteKey string, bodyType sProto.Body_Type) error {
|
||||||
|
msg, err := signal.MarshalCredential(
|
||||||
|
s.wgPrivateKey,
|
||||||
|
offerAnswer.WgListenPort,
|
||||||
|
remoteKey,
|
||||||
|
&signal.Credential{
|
||||||
|
UFrag: offerAnswer.IceCredentials.UFrag,
|
||||||
|
Pwd: offerAnswer.IceCredentials.Pwd,
|
||||||
|
},
|
||||||
|
bodyType,
|
||||||
|
offerAnswer.RosenpassPubKey,
|
||||||
|
offerAnswer.RosenpassAddr,
|
||||||
|
offerAnswer.RelaySrvAddress)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.signal.Send(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -2,14 +2,18 @@ package peer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
gstatus "google.golang.org/grpc/status"
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/relay"
|
"github.com/netbirdio/netbird/client/internal/relay"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
|
relayClient "github.com/netbirdio/netbird/relay/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// State contains the latest state of a peer
|
// State contains the latest state of a peer
|
||||||
@@ -37,25 +41,25 @@ type State struct {
|
|||||||
// AddRoute add a single route to routes map
|
// AddRoute add a single route to routes map
|
||||||
func (s *State) AddRoute(network string) {
|
func (s *State) AddRoute(network string) {
|
||||||
s.Mux.Lock()
|
s.Mux.Lock()
|
||||||
|
defer s.Mux.Unlock()
|
||||||
if s.routes == nil {
|
if s.routes == nil {
|
||||||
s.routes = make(map[string]struct{})
|
s.routes = make(map[string]struct{})
|
||||||
}
|
}
|
||||||
s.routes[network] = struct{}{}
|
s.routes[network] = struct{}{}
|
||||||
s.Mux.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRoutes set state routes
|
// SetRoutes set state routes
|
||||||
func (s *State) SetRoutes(routes map[string]struct{}) {
|
func (s *State) SetRoutes(routes map[string]struct{}) {
|
||||||
s.Mux.Lock()
|
s.Mux.Lock()
|
||||||
|
defer s.Mux.Unlock()
|
||||||
s.routes = routes
|
s.routes = routes
|
||||||
s.Mux.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteRoute removes a route from the network amp
|
// DeleteRoute removes a route from the network amp
|
||||||
func (s *State) DeleteRoute(network string) {
|
func (s *State) DeleteRoute(network string) {
|
||||||
s.Mux.Lock()
|
s.Mux.Lock()
|
||||||
|
defer s.Mux.Unlock()
|
||||||
delete(s.routes, network)
|
delete(s.routes, network)
|
||||||
s.Mux.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRoutes return routes map
|
// GetRoutes return routes map
|
||||||
@@ -117,40 +121,50 @@ type FullStatus struct {
|
|||||||
|
|
||||||
// Status holds a state of peers, signal, management connections and relays
|
// Status holds a state of peers, signal, management connections and relays
|
||||||
type Status struct {
|
type Status struct {
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
peers map[string]State
|
peers map[string]State
|
||||||
changeNotify map[string]chan struct{}
|
changeNotify map[string]chan struct{}
|
||||||
signalState bool
|
signalState bool
|
||||||
signalError error
|
signalError error
|
||||||
managementState bool
|
managementState bool
|
||||||
managementError error
|
managementError error
|
||||||
relayStates []relay.ProbeResult
|
relayStates []relay.ProbeResult
|
||||||
localPeer LocalPeerState
|
localPeer LocalPeerState
|
||||||
offlinePeers []State
|
offlinePeers []State
|
||||||
mgmAddress string
|
mgmAddress string
|
||||||
signalAddress string
|
signalAddress string
|
||||||
notifier *notifier
|
notifier *notifier
|
||||||
rosenpassEnabled bool
|
rosenpassEnabled bool
|
||||||
rosenpassPermissive bool
|
rosenpassPermissive bool
|
||||||
nsGroupStates []NSGroupState
|
nsGroupStates []NSGroupState
|
||||||
|
resolvedDomainsStates map[domain.Domain][]netip.Prefix
|
||||||
|
|
||||||
// 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
|
||||||
// set to true this variable and at the end of the processing we will reset it by the FinishPeerListModifications()
|
// set to true this variable and at the end of the processing we will reset it by the FinishPeerListModifications()
|
||||||
peerListChangedForNotification bool
|
peerListChangedForNotification bool
|
||||||
|
|
||||||
|
relayMgr *relayClient.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRecorder returns a new Status instance
|
// NewRecorder returns a new Status instance
|
||||||
func NewRecorder(mgmAddress string) *Status {
|
func NewRecorder(mgmAddress string) *Status {
|
||||||
return &Status{
|
return &Status{
|
||||||
peers: make(map[string]State),
|
peers: make(map[string]State),
|
||||||
changeNotify: make(map[string]chan struct{}),
|
changeNotify: make(map[string]chan struct{}),
|
||||||
offlinePeers: make([]State, 0),
|
offlinePeers: make([]State, 0),
|
||||||
notifier: newNotifier(),
|
notifier: newNotifier(),
|
||||||
mgmAddress: mgmAddress,
|
mgmAddress: mgmAddress,
|
||||||
|
resolvedDomainsStates: make(map[domain.Domain][]netip.Prefix),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Status) SetRelayMgr(manager *relayClient.Manager) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
d.relayMgr = manager
|
||||||
|
}
|
||||||
|
|
||||||
// ReplaceOfflinePeers replaces
|
// ReplaceOfflinePeers replaces
|
||||||
func (d *Status) ReplaceOfflinePeers(replacement []State) {
|
func (d *Status) ReplaceOfflinePeers(replacement []State) {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
@@ -188,7 +202,7 @@ func (d *Status) GetPeer(peerPubKey string) (State, error) {
|
|||||||
|
|
||||||
state, ok := d.peers[peerPubKey]
|
state, ok := d.peers[peerPubKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
return State{}, errors.New("peer not found")
|
return State{}, iface.ErrPeerNotFound
|
||||||
}
|
}
|
||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
@@ -429,6 +443,18 @@ func (d *Status) UpdateDNSStates(dnsStates []NSGroupState) {
|
|||||||
d.nsGroupStates = dnsStates
|
d.nsGroupStates = dnsStates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Status) UpdateResolvedDomainsStates(domain domain.Domain, prefixes []netip.Prefix) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
d.resolvedDomainsStates[domain] = prefixes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Status) DeleteResolvedDomainsStates(domain domain.Domain) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
delete(d.resolvedDomainsStates, domain)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Status) GetRosenpassState() RosenpassState {
|
func (d *Status) GetRosenpassState() RosenpassState {
|
||||||
return RosenpassState{
|
return RosenpassState{
|
||||||
d.rosenpassEnabled,
|
d.rosenpassEnabled,
|
||||||
@@ -486,13 +512,40 @@ func (d *Status) GetSignalState() SignalState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetRelayStates() []relay.ProbeResult {
|
func (d *Status) GetRelayStates() []relay.ProbeResult {
|
||||||
return d.relayStates
|
if d.relayMgr == nil {
|
||||||
|
return d.relayStates
|
||||||
|
}
|
||||||
|
|
||||||
|
// extend the list of stun, turn servers with relay address
|
||||||
|
relaysState := make([]relay.ProbeResult, len(d.relayStates), len(d.relayStates)+1)
|
||||||
|
copy(relaysState, d.relayStates)
|
||||||
|
|
||||||
|
relayState := relay.ProbeResult{}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
instanceAddr, err := d.relayMgr.RelayInstanceAddress()
|
||||||
|
if err != nil {
|
||||||
|
relayState.URI = d.relayMgr.ServerURL()
|
||||||
|
relayState.Err = err
|
||||||
|
} else {
|
||||||
|
relayState.URI = instanceAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
relaysState = append(relaysState, relayState)
|
||||||
|
return relaysState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetDNSStates() []NSGroupState {
|
func (d *Status) GetDNSStates() []NSGroupState {
|
||||||
return d.nsGroupStates
|
return d.nsGroupStates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Status) GetResolvedDomainsStates() map[domain.Domain][]netip.Prefix {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
return maps.Clone(d.resolvedDomainsStates)
|
||||||
|
}
|
||||||
|
|
||||||
// GetFullStatus gets full status
|
// GetFullStatus gets full status
|
||||||
func (d *Status) GetFullStatus() FullStatus {
|
func (d *Status) GetFullStatus() FullStatus {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package peer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -43,7 +43,7 @@ func TestUpdatePeerState(t *testing.T) {
|
|||||||
status := NewRecorder("https://mgm")
|
status := NewRecorder("https://mgm")
|
||||||
peerState := State{
|
peerState := State{
|
||||||
PubKey: key,
|
PubKey: key,
|
||||||
Mux: new(sync.RWMutex),
|
Mux: new(sync.RWMutex),
|
||||||
}
|
}
|
||||||
|
|
||||||
status.peers[key] = peerState
|
status.peers[key] = peerState
|
||||||
@@ -64,7 +64,7 @@ func TestStatus_UpdatePeerFQDN(t *testing.T) {
|
|||||||
status := NewRecorder("https://mgm")
|
status := NewRecorder("https://mgm")
|
||||||
peerState := State{
|
peerState := State{
|
||||||
PubKey: key,
|
PubKey: key,
|
||||||
Mux: new(sync.RWMutex),
|
Mux: new(sync.RWMutex),
|
||||||
}
|
}
|
||||||
|
|
||||||
status.peers[key] = peerState
|
status.peers[key] = peerState
|
||||||
@@ -83,7 +83,7 @@ func TestGetPeerStateChangeNotifierLogic(t *testing.T) {
|
|||||||
status := NewRecorder("https://mgm")
|
status := NewRecorder("https://mgm")
|
||||||
peerState := State{
|
peerState := State{
|
||||||
PubKey: key,
|
PubKey: key,
|
||||||
Mux: new(sync.RWMutex),
|
Mux: new(sync.RWMutex),
|
||||||
}
|
}
|
||||||
|
|
||||||
status.peers[key] = peerState
|
status.peers[key] = peerState
|
||||||
@@ -108,7 +108,7 @@ func TestRemovePeer(t *testing.T) {
|
|||||||
status := NewRecorder("https://mgm")
|
status := NewRecorder("https://mgm")
|
||||||
peerState := State{
|
peerState := State{
|
||||||
PubKey: key,
|
PubKey: key,
|
||||||
Mux: new(sync.RWMutex),
|
Mux: new(sync.RWMutex),
|
||||||
}
|
}
|
||||||
|
|
||||||
status.peers[key] = peerState
|
status.peers[key] = peerState
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (conn *Conn) newStdNet() (*stdnet.Net, error) {
|
func (w *WorkerICE) newStdNet() (*stdnet.Net, error) {
|
||||||
return stdnet.NewNet(conn.config.InterfaceBlackList)
|
return stdnet.NewNet(w.configICE.InterfaceBlackList)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ package peer
|
|||||||
|
|
||||||
import "github.com/netbirdio/netbird/client/internal/stdnet"
|
import "github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
|
|
||||||
func (conn *Conn) newStdNet() (*stdnet.Net, error) {
|
func (w *WorkerICE) newStdNet() (*stdnet.Net, error) {
|
||||||
return stdnet.NewNetWithDiscover(conn.iFaceDiscover, conn.config.InterfaceBlackList)
|
return stdnet.NewNetWithDiscover(w.iFaceDiscover, w.configICE.InterfaceBlackList)
|
||||||
}
|
}
|
||||||
|
|||||||
461
client/internal/peer/worker_ice.go
Normal file
461
client/internal/peer/worker_ice.go
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pion/ice/v3"
|
||||||
|
"github.com/pion/randutil"
|
||||||
|
"github.com/pion/stun/v2"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/iface/bind"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
iceKeepAliveDefault = 4 * time.Second
|
||||||
|
iceDisconnectedTimeoutDefault = 6 * time.Second
|
||||||
|
// iceRelayAcceptanceMinWaitDefault is the same as in the Pion ICE package
|
||||||
|
iceRelayAcceptanceMinWaitDefault = 2 * time.Second
|
||||||
|
|
||||||
|
lenUFrag = 16
|
||||||
|
lenPwd = 32
|
||||||
|
runesAlpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
failedTimeout = 6 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type ICEConfig struct {
|
||||||
|
// StunTurn is a list of STUN and TURN URLs
|
||||||
|
StunTurn atomic.Value // []*stun.URI
|
||||||
|
|
||||||
|
// InterfaceBlackList is a list of machine interfaces that should be filtered out by ICE Candidate gathering
|
||||||
|
// (e.g. if eth0 is in the list, host candidate of this interface won't be used)
|
||||||
|
InterfaceBlackList []string
|
||||||
|
DisableIPv6Discovery bool
|
||||||
|
|
||||||
|
UDPMux ice.UDPMux
|
||||||
|
UDPMuxSrflx ice.UniversalUDPMux
|
||||||
|
|
||||||
|
NATExternalIPs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ICEConnInfo struct {
|
||||||
|
RemoteConn net.Conn
|
||||||
|
RosenpassPubKey []byte
|
||||||
|
RosenpassAddr string
|
||||||
|
LocalIceCandidateType string
|
||||||
|
RemoteIceCandidateType string
|
||||||
|
RemoteIceCandidateEndpoint string
|
||||||
|
LocalIceCandidateEndpoint string
|
||||||
|
Direct bool
|
||||||
|
Relayed bool
|
||||||
|
RelayedOnLocal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkerICECallbacks struct {
|
||||||
|
OnConnReady func(ConnPriority, ICEConnInfo)
|
||||||
|
OnStatusChanged func(ConnStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkerICE struct {
|
||||||
|
ctx context.Context
|
||||||
|
log *log.Entry
|
||||||
|
config ConnConfig
|
||||||
|
configICE ICEConfig
|
||||||
|
signaler *Signaler
|
||||||
|
iFaceDiscover stdnet.ExternalIFaceDiscover
|
||||||
|
statusRecorder *Status
|
||||||
|
hasRelayOnLocally bool
|
||||||
|
conn WorkerICECallbacks
|
||||||
|
|
||||||
|
selectedPriority ConnPriority
|
||||||
|
|
||||||
|
agent *ice.Agent
|
||||||
|
muxAgent sync.Mutex
|
||||||
|
|
||||||
|
StunTurn []*stun.URI
|
||||||
|
|
||||||
|
sentExtraSrflx bool
|
||||||
|
|
||||||
|
localUfrag string
|
||||||
|
localPwd string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorkerICE(ctx context.Context, log *log.Entry, config ConnConfig, configICE ICEConfig, signaler *Signaler, ifaceDiscover stdnet.ExternalIFaceDiscover, statusRecorder *Status, hasRelayOnLocally bool, callBacks WorkerICECallbacks) (*WorkerICE, error) {
|
||||||
|
w := &WorkerICE{
|
||||||
|
ctx: ctx,
|
||||||
|
log: log,
|
||||||
|
config: config,
|
||||||
|
configICE: configICE,
|
||||||
|
signaler: signaler,
|
||||||
|
iFaceDiscover: ifaceDiscover,
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
|
hasRelayOnLocally: hasRelayOnLocally,
|
||||||
|
conn: callBacks,
|
||||||
|
}
|
||||||
|
|
||||||
|
localUfrag, localPwd, err := generateICECredentials()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.localUfrag = localUfrag
|
||||||
|
w.localPwd = localPwd
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerICE) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
||||||
|
w.log.Debugf("OnNewOffer for ICE")
|
||||||
|
w.muxAgent.Lock()
|
||||||
|
|
||||||
|
if w.agent != nil {
|
||||||
|
w.log.Debugf("agent already exists, skipping the offer")
|
||||||
|
w.muxAgent.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var preferredCandidateTypes []ice.CandidateType
|
||||||
|
if w.hasRelayOnLocally && remoteOfferAnswer.RelaySrvAddress != "" {
|
||||||
|
w.selectedPriority = connPriorityICEP2P
|
||||||
|
preferredCandidateTypes = candidateTypesP2P()
|
||||||
|
} else {
|
||||||
|
w.selectedPriority = connPriorityICETurn
|
||||||
|
preferredCandidateTypes = candidateTypes()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.log.Debugf("recreate ICE agent")
|
||||||
|
agentCtx, agentCancel := context.WithCancel(w.ctx)
|
||||||
|
agent, err := w.reCreateAgent(agentCancel, preferredCandidateTypes)
|
||||||
|
if err != nil {
|
||||||
|
w.log.Errorf("failed to recreate ICE Agent: %s", err)
|
||||||
|
w.muxAgent.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.agent = agent
|
||||||
|
w.muxAgent.Unlock()
|
||||||
|
|
||||||
|
w.log.Debugf("gather candidates")
|
||||||
|
err = w.agent.GatherCandidates()
|
||||||
|
if err != nil {
|
||||||
|
w.log.Debugf("failed to gather candidates: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// will block until connection succeeded
|
||||||
|
// but it won't release if ICE Agent went into Disconnected or Failed state,
|
||||||
|
// so we have to cancel it with the provided context once agent detected a broken connection
|
||||||
|
w.log.Debugf("turn agent dial")
|
||||||
|
remoteConn, err := w.turnAgentDial(agentCtx, remoteOfferAnswer)
|
||||||
|
if err != nil {
|
||||||
|
w.log.Debugf("failed to dial the remote peer: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.log.Debugf("agent dial succeeded")
|
||||||
|
|
||||||
|
pair, err := w.agent.GetSelectedCandidatePair()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isRelayCandidate(pair.Local) {
|
||||||
|
// dynamically set remote WireGuard port if other side specified a different one from the default one
|
||||||
|
remoteWgPort := iface.DefaultWgPort
|
||||||
|
if remoteOfferAnswer.WgListenPort != 0 {
|
||||||
|
remoteWgPort = remoteOfferAnswer.WgListenPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// To support old version's with direct mode we attempt to punch an additional role with the remote WireGuard port
|
||||||
|
go w.punchRemoteWGPort(pair, remoteWgPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
ci := ICEConnInfo{
|
||||||
|
RemoteConn: remoteConn,
|
||||||
|
RosenpassPubKey: remoteOfferAnswer.RosenpassPubKey,
|
||||||
|
RosenpassAddr: remoteOfferAnswer.RosenpassAddr,
|
||||||
|
LocalIceCandidateType: pair.Local.Type().String(),
|
||||||
|
RemoteIceCandidateType: pair.Remote.Type().String(),
|
||||||
|
LocalIceCandidateEndpoint: fmt.Sprintf("%s:%d", pair.Local.Address(), pair.Local.Port()),
|
||||||
|
RemoteIceCandidateEndpoint: fmt.Sprintf("%s:%d", pair.Remote.Address(), pair.Remote.Port()),
|
||||||
|
Direct: !isRelayCandidate(pair.Local),
|
||||||
|
Relayed: isRelayed(pair),
|
||||||
|
RelayedOnLocal: isRelayCandidate(pair.Local),
|
||||||
|
}
|
||||||
|
w.log.Debugf("on ICE conn read to use ready")
|
||||||
|
go w.conn.OnConnReady(w.selectedPriority, ci)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer.
|
||||||
|
func (w *WorkerICE) OnRemoteCandidate(candidate ice.Candidate, haRoutes route.HAMap) {
|
||||||
|
w.muxAgent.Lock()
|
||||||
|
defer w.muxAgent.Unlock()
|
||||||
|
w.log.Debugf("OnRemoteCandidate from peer %s -> %s", w.config.Key, candidate.String())
|
||||||
|
if w.agent == nil {
|
||||||
|
w.log.Warnf("ICE Agent is not initialized yet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if candidateViaRoutes(candidate, haRoutes) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := w.agent.AddRemoteCandidate(candidate)
|
||||||
|
if err != nil {
|
||||||
|
w.log.Errorf("error while handling remote candidate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerICE) GetLocalUserCredentials() (frag string, pwd string) {
|
||||||
|
w.muxAgent.Lock()
|
||||||
|
defer w.muxAgent.Unlock()
|
||||||
|
return w.localUfrag, w.localPwd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerICE) reCreateAgent(agentCancel context.CancelFunc, relaySupport []ice.CandidateType) (*ice.Agent, error) {
|
||||||
|
transportNet, err := w.newStdNet()
|
||||||
|
if err != nil {
|
||||||
|
w.log.Errorf("failed to create pion's stdnet: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
iceKeepAlive := iceKeepAlive()
|
||||||
|
iceDisconnectedTimeout := iceDisconnectedTimeout()
|
||||||
|
iceRelayAcceptanceMinWait := iceRelayAcceptanceMinWait()
|
||||||
|
|
||||||
|
agentConfig := &ice.AgentConfig{
|
||||||
|
MulticastDNSMode: ice.MulticastDNSModeDisabled,
|
||||||
|
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6},
|
||||||
|
Urls: w.configICE.StunTurn.Load().([]*stun.URI),
|
||||||
|
CandidateTypes: relaySupport,
|
||||||
|
InterfaceFilter: stdnet.InterfaceFilter(w.configICE.InterfaceBlackList),
|
||||||
|
UDPMux: w.configICE.UDPMux,
|
||||||
|
UDPMuxSrflx: w.configICE.UDPMuxSrflx,
|
||||||
|
NAT1To1IPs: w.configICE.NATExternalIPs,
|
||||||
|
Net: transportNet,
|
||||||
|
FailedTimeout: &failedTimeout,
|
||||||
|
DisconnectedTimeout: &iceDisconnectedTimeout,
|
||||||
|
KeepaliveInterval: &iceKeepAlive,
|
||||||
|
RelayAcceptanceMinWait: &iceRelayAcceptanceMinWait,
|
||||||
|
LocalUfrag: w.localUfrag,
|
||||||
|
LocalPwd: w.localPwd,
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.configICE.DisableIPv6Discovery {
|
||||||
|
agentConfig.NetworkTypes = []ice.NetworkType{ice.NetworkTypeUDP4}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.sentExtraSrflx = false
|
||||||
|
agent, err := ice.NewAgent(agentConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = agent.OnCandidate(w.onICECandidate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = agent.OnConnectionStateChange(func(state ice.ConnectionState) {
|
||||||
|
w.log.Debugf("ICE ConnectionState has changed to %s", state.String())
|
||||||
|
if state == ice.ConnectionStateFailed || state == ice.ConnectionStateDisconnected {
|
||||||
|
w.conn.OnStatusChanged(StatusDisconnected)
|
||||||
|
|
||||||
|
w.muxAgent.Lock()
|
||||||
|
agentCancel()
|
||||||
|
_ = agent.Close()
|
||||||
|
w.agent = nil
|
||||||
|
|
||||||
|
w.muxAgent.Unlock()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = agent.OnSelectedCandidatePairChange(w.onICESelectedCandidatePair)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = agent.OnSuccessfulSelectedPairBindingResponse(func(p *ice.CandidatePair) {
|
||||||
|
err := w.statusRecorder.UpdateLatency(w.config.Key, p.Latency())
|
||||||
|
if err != nil {
|
||||||
|
w.log.Debugf("failed to update latency for peer: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed setting binding response callback: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return agent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerICE) punchRemoteWGPort(pair *ice.CandidatePair, remoteWgPort int) {
|
||||||
|
// wait local endpoint configuration
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", pair.Remote.Address(), remoteWgPort))
|
||||||
|
if err != nil {
|
||||||
|
w.log.Warnf("got an error while resolving the udp address, err: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mux, ok := w.configICE.UDPMuxSrflx.(*bind.UniversalUDPMuxDefault)
|
||||||
|
if !ok {
|
||||||
|
w.log.Warn("invalid udp mux conversion")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = mux.GetSharedConn().WriteTo([]byte{0x6e, 0x62}, addr)
|
||||||
|
if err != nil {
|
||||||
|
w.log.Warnf("got an error while sending the punch packet, err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// onICECandidate is a callback attached to an ICE Agent to receive new local connection candidates
|
||||||
|
// and then signals them to the remote peer
|
||||||
|
func (w *WorkerICE) onICECandidate(candidate ice.Candidate) {
|
||||||
|
// nil means candidate gathering has been ended
|
||||||
|
if candidate == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: reported port is incorrect for CandidateTypeHost, makes understanding ICE use via logs confusing as port is ignored
|
||||||
|
w.log.Debugf("discovered local candidate %s", candidate.String())
|
||||||
|
go func() {
|
||||||
|
err := w.signaler.SignalICECandidate(candidate, w.config.Key)
|
||||||
|
if err != nil {
|
||||||
|
w.log.Errorf("failed signaling candidate to the remote peer %s %s", w.config.Key, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !w.shouldSendExtraSrflxCandidate(candidate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends an extra server reflexive candidate to the remote peer with our related port (usually the wireguard port)
|
||||||
|
// this is useful when network has an existing port forwarding rule for the wireguard port and this peer
|
||||||
|
extraSrflx, err := extraSrflxCandidate(candidate)
|
||||||
|
if err != nil {
|
||||||
|
w.log.Errorf("failed creating extra server reflexive candidate %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.sentExtraSrflx = true
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err = w.signaler.SignalICECandidate(extraSrflx, w.config.Key)
|
||||||
|
if err != nil {
|
||||||
|
w.log.Errorf("failed signaling the extra server reflexive candidate: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerICE) onICESelectedCandidatePair(c1 ice.Candidate, c2 ice.Candidate) {
|
||||||
|
w.log.Debugf("selected candidate pair [local <-> remote] -> [%s <-> %s], peer %s", c1.String(), c2.String(),
|
||||||
|
w.config.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerICE) shouldSendExtraSrflxCandidate(candidate ice.Candidate) bool {
|
||||||
|
if !w.sentExtraSrflx && candidate.Type() == ice.CandidateTypeServerReflexive && candidate.Port() != candidate.RelatedAddress().Port {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerICE) turnAgentDial(ctx context.Context, remoteOfferAnswer *OfferAnswer) (*ice.Conn, error) {
|
||||||
|
isControlling := w.config.LocalKey > w.config.Key
|
||||||
|
if isControlling {
|
||||||
|
return w.agent.Dial(ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
|
||||||
|
} else {
|
||||||
|
return w.agent.Accept(ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extraSrflxCandidate(candidate ice.Candidate) (*ice.CandidateServerReflexive, error) {
|
||||||
|
relatedAdd := candidate.RelatedAddress()
|
||||||
|
return ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
|
||||||
|
Network: candidate.NetworkType().String(),
|
||||||
|
Address: candidate.Address(),
|
||||||
|
Port: relatedAdd.Port,
|
||||||
|
Component: candidate.Component(),
|
||||||
|
RelAddr: relatedAdd.Address,
|
||||||
|
RelPort: relatedAdd.Port,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func candidateViaRoutes(candidate ice.Candidate, clientRoutes route.HAMap) bool {
|
||||||
|
var routePrefixes []netip.Prefix
|
||||||
|
for _, routes := range clientRoutes {
|
||||||
|
if len(routes) > 0 && routes[0] != nil {
|
||||||
|
routePrefixes = append(routePrefixes, routes[0].Network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := netip.ParseAddr(candidate.Address())
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to parse IP address %s: %v", candidate.Address(), err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prefix := range routePrefixes {
|
||||||
|
// default route is
|
||||||
|
if prefix.Bits() == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix.Contains(addr) {
|
||||||
|
log.Debugf("Ignoring candidate [%s], its address is part of routed network %s", candidate.String(), prefix)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func candidateTypes() []ice.CandidateType {
|
||||||
|
if hasICEForceRelayConn() {
|
||||||
|
return []ice.CandidateType{ice.CandidateTypeRelay}
|
||||||
|
}
|
||||||
|
// TODO: remove this once we have refactored userspace proxy into the bind package
|
||||||
|
if runtime.GOOS == "ios" {
|
||||||
|
return []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive}
|
||||||
|
}
|
||||||
|
return []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive, ice.CandidateTypeRelay}
|
||||||
|
}
|
||||||
|
|
||||||
|
func candidateTypesP2P() []ice.CandidateType {
|
||||||
|
return []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRelayCandidate(candidate ice.Candidate) bool {
|
||||||
|
return candidate.Type() == ice.CandidateTypeRelay
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRelayed(pair *ice.CandidatePair) bool {
|
||||||
|
if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateICECredentials() (string, string, error) {
|
||||||
|
ufrag, err := randutil.GenerateCryptoRandomString(lenUFrag, runesAlpha)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
pwd, err := randutil.GenerateCryptoRandomString(lenPwd, runesAlpha)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return ufrag, pwd, nil
|
||||||
|
|
||||||
|
}
|
||||||
100
client/internal/peer/worker_relay.go
Normal file
100
client/internal/peer/worker_relay.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
relayClient "github.com/netbirdio/netbird/relay/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RelayConnInfo struct {
|
||||||
|
relayedConn net.Conn
|
||||||
|
rosenpassPubKey []byte
|
||||||
|
rosenpassAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkerRelayCallbacks struct {
|
||||||
|
OnConnReady func(RelayConnInfo)
|
||||||
|
OnDisconnected func()
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkerRelay struct {
|
||||||
|
ctx context.Context
|
||||||
|
log *log.Entry
|
||||||
|
config ConnConfig
|
||||||
|
relayManager *relayClient.Manager
|
||||||
|
conn WorkerRelayCallbacks
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorkerRelay(ctx context.Context, log *log.Entry, config ConnConfig, relayManager *relayClient.Manager, callbacks WorkerRelayCallbacks) *WorkerRelay {
|
||||||
|
return &WorkerRelay{
|
||||||
|
ctx: ctx,
|
||||||
|
log: log,
|
||||||
|
config: config,
|
||||||
|
relayManager: relayManager,
|
||||||
|
conn: callbacks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
||||||
|
if !w.isRelaySupported(remoteOfferAnswer) {
|
||||||
|
w.log.Infof("Relay is not supported by remote peer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// the relayManager will return with error in case if the connection has lost with relay server
|
||||||
|
currentRelayAddress, err := w.relayManager.RelayInstanceAddress()
|
||||||
|
if err != nil {
|
||||||
|
w.log.Infof("local Relay connection is lost, skipping connection attempt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := w.preferredRelayServer(currentRelayAddress, remoteOfferAnswer.RelaySrvAddress)
|
||||||
|
|
||||||
|
relayedConn, err := w.relayManager.OpenConn(srv, w.config.Key, w.conn.OnDisconnected)
|
||||||
|
if err != nil {
|
||||||
|
// todo handle all type errors
|
||||||
|
if errors.Is(err, relayClient.ErrConnAlreadyExists) {
|
||||||
|
w.log.Infof("do not need to reopen relay connection")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.log.Infof("do not need to reopen relay connection: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.log.Debugf("Relay connection established with %s", srv)
|
||||||
|
go w.conn.OnConnReady(RelayConnInfo{
|
||||||
|
relayedConn: relayedConn,
|
||||||
|
rosenpassPubKey: remoteOfferAnswer.RosenpassPubKey,
|
||||||
|
rosenpassAddr: remoteOfferAnswer.RosenpassAddr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerRelay) RelayInstanceAddress() (string, error) {
|
||||||
|
return w.relayManager.RelayInstanceAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerRelay) IsController() bool {
|
||||||
|
return w.config.LocalKey > w.config.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerRelay) RelayIsSupportedLocally() bool {
|
||||||
|
return w.relayManager.HasRelayAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerRelay) isRelaySupported(answer *OfferAnswer) bool {
|
||||||
|
if !w.relayManager.HasRelayAddress() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return answer.RelaySrvAddress != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkerRelay) preferredRelayServer(myRelayAddress, remoteRelayAddress string) string {
|
||||||
|
if w.IsController() {
|
||||||
|
return myRelayAddress
|
||||||
|
}
|
||||||
|
return remoteRelayAddress
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
|
|
||||||
// ProbeResult holds the info about the result of a relay probe request
|
// ProbeResult holds the info about the result of a relay probe request
|
||||||
type ProbeResult struct {
|
type ProbeResult struct {
|
||||||
URI *stun.URI
|
URI string
|
||||||
Err error
|
Err error
|
||||||
Addr string
|
Addr string
|
||||||
}
|
}
|
||||||
@@ -170,13 +170,13 @@ func ProbeAll(
|
|||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for i, uri := range relays {
|
for i, uri := range relays {
|
||||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(res *ProbeResult, stunURI *stun.URI) {
|
go func(res *ProbeResult, stunURI *stun.URI) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
res.URI = stunURI
|
res.URI = stunURI.String()
|
||||||
res.Addr, res.Err = fn(ctx, stunURI)
|
res.Addr, res.Err = fn(ctx, stunURI)
|
||||||
}(&results[i], uri)
|
}(&results[i], uri)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,20 @@ package routemanager
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/static"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
const minRangeBits = 7
|
|
||||||
|
|
||||||
type routerPeerStatus struct {
|
type routerPeerStatus struct {
|
||||||
connected bool
|
connected bool
|
||||||
relayed bool
|
relayed bool
|
||||||
@@ -27,39 +29,48 @@ type routesUpdate struct {
|
|||||||
routes []*route.Route
|
routes []*route.Route
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RouteHandler defines the interface for handling routes
|
||||||
|
type RouteHandler interface {
|
||||||
|
String() string
|
||||||
|
AddRoute(ctx context.Context) error
|
||||||
|
RemoveRoute() error
|
||||||
|
AddAllowedIPs(peerKey string) error
|
||||||
|
RemoveAllowedIPs() error
|
||||||
|
}
|
||||||
|
|
||||||
type clientNetwork struct {
|
type clientNetwork struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
stop context.CancelFunc
|
cancel context.CancelFunc
|
||||||
statusRecorder *peer.Status
|
statusRecorder *peer.Status
|
||||||
wgInterface *iface.WGIface
|
wgInterface iface.IWGIface
|
||||||
routes map[string]*route.Route
|
routes map[route.ID]*route.Route
|
||||||
routeUpdate chan routesUpdate
|
routeUpdate chan routesUpdate
|
||||||
peerStateUpdate chan struct{}
|
peerStateUpdate chan struct{}
|
||||||
routePeersNotifiers map[string]chan struct{}
|
routePeersNotifiers map[string]chan struct{}
|
||||||
chosenRoute *route.Route
|
currentChosen *route.Route
|
||||||
network netip.Prefix
|
handler RouteHandler
|
||||||
updateSerial uint64
|
updateSerial uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func newClientNetworkWatcher(ctx context.Context, wgInterface *iface.WGIface, statusRecorder *peer.Status, network netip.Prefix) *clientNetwork {
|
func newClientNetworkWatcher(ctx context.Context, dnsRouteInterval time.Duration, wgInterface iface.IWGIface, statusRecorder *peer.Status, rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allowedIPsRefCounter *refcounter.AllowedIPsRefCounter) *clientNetwork {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
client := &clientNetwork{
|
client := &clientNetwork{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
stop: cancel,
|
cancel: cancel,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
wgInterface: wgInterface,
|
wgInterface: wgInterface,
|
||||||
routes: make(map[string]*route.Route),
|
routes: make(map[route.ID]*route.Route),
|
||||||
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{}),
|
||||||
network: network,
|
handler: handlerFromRoute(rt, routeRefCounter, allowedIPsRefCounter, dnsRouteInterval, statusRecorder),
|
||||||
}
|
}
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *clientNetwork) getRouterPeerStatuses() map[string]routerPeerStatus {
|
func (c *clientNetwork) getRouterPeerStatuses() map[route.ID]routerPeerStatus {
|
||||||
routePeerStatuses := make(map[string]routerPeerStatus)
|
routePeerStatuses := make(map[route.ID]routerPeerStatus)
|
||||||
for _, r := range c.routes {
|
for _, r := range c.routes {
|
||||||
peerStatus, err := c.statusRecorder.GetPeer(r.Peer)
|
peerStatus, err := c.statusRecorder.GetPeer(r.Peer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -85,18 +96,18 @@ func (c *clientNetwork) getRouterPeerStatuses() map[string]routerPeerStatus {
|
|||||||
// * Metric: Routes with lower metrics (better) are prioritized.
|
// * Metric: Routes with lower metrics (better) are prioritized.
|
||||||
// * Non-relayed: Routes without relays are preferred.
|
// * Non-relayed: Routes without relays are preferred.
|
||||||
// * Direct connections: Routes with direct peer connections are favored.
|
// * Direct connections: Routes with direct peer connections are favored.
|
||||||
// * Stability: In case of equal scores, the currently active route (if any) is maintained.
|
|
||||||
// * Latency: Routes with lower latency are prioritized.
|
// * Latency: Routes with lower latency are prioritized.
|
||||||
|
// * Stability: In case of equal scores, the currently active route (if any) is maintained.
|
||||||
//
|
//
|
||||||
// It returns the ID of the selected optimal route.
|
// It returns the ID of the selected optimal route.
|
||||||
func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[string]routerPeerStatus) string {
|
func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID]routerPeerStatus) route.ID {
|
||||||
chosen := ""
|
chosen := route.ID("")
|
||||||
chosenScore := float64(0)
|
chosenScore := float64(0)
|
||||||
currScore := float64(0)
|
currScore := float64(0)
|
||||||
|
|
||||||
currID := ""
|
currID := route.ID("")
|
||||||
if c.chosenRoute != nil {
|
if c.currentChosen != nil {
|
||||||
currID = c.chosenRoute.ID
|
currID = c.currentChosen.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range c.routes {
|
for _, r := range c.routes {
|
||||||
@@ -150,13 +161,18 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[string]ro
|
|||||||
peers = append(peers, r.Peer)
|
peers = append(peers, r.Peer)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Warnf("the network %s has not been assigned a routing peer as no peers from the list %s are currently connected", c.network, peers)
|
log.Warnf("The network [%v] has not been assigned a routing peer as no peers from the list %s are currently connected", c.handler, peers)
|
||||||
case chosen != currID:
|
case chosen != currID:
|
||||||
if currScore != 0 && currScore < chosenScore+0.1 {
|
// we compare the current score + 10ms to the chosen score to avoid flapping between routes
|
||||||
|
if currScore != 0 && currScore+0.01 > chosenScore {
|
||||||
|
log.Debugf("Keeping current routing peer because the score difference with latency is less than 0.01(10ms), current: %f, new: %f", currScore, chosenScore)
|
||||||
return currID
|
return currID
|
||||||
} else {
|
|
||||||
log.Infof("new chosen route is %s with peer %s with score %f for network %s", chosen, c.routes[chosen].Peer, chosenScore, c.network)
|
|
||||||
}
|
}
|
||||||
|
var p string
|
||||||
|
if rt := c.routes[chosen]; rt != nil {
|
||||||
|
p = rt.Peer
|
||||||
|
}
|
||||||
|
log.Infof("New chosen route is %s with peer %s with score %f for network [%v]", chosen, p, chosenScore, c.handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
return chosen
|
return chosen
|
||||||
@@ -190,98 +206,103 @@ func (c *clientNetwork) startPeersStatusChangeWatcher() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error {
|
func (c *clientNetwork) removeRouteFromWireguardPeer() error {
|
||||||
state, err := c.statusRecorder.GetPeer(peerKey)
|
c.removeStateRoute()
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get peer state: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
state.DeleteRoute(c.network.String())
|
if err := c.handler.RemoveAllowedIPs(); err != nil {
|
||||||
if err := c.statusRecorder.UpdatePeerState(state); err != nil {
|
return fmt.Errorf("remove allowed IPs: %w", err)
|
||||||
log.Warnf("Failed to update peer state: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.ConnStatus != peer.StatusConnected {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.wgInterface.RemoveAllowedIP(peerKey, c.network.String())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("remove allowed IP %s removed for peer %s, err: %v",
|
|
||||||
c.network, c.chosenRoute.Peer, err)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
|
func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
|
||||||
if c.chosenRoute != nil {
|
if c.currentChosen == nil {
|
||||||
if err := removeVPNRoute(c.network, c.wgInterface.Name()); err != nil {
|
return nil
|
||||||
return fmt.Errorf("remove route %s from system, err: %v", c.network, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer); err != nil {
|
|
||||||
return fmt.Errorf("remove route: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
|
||||||
|
if err := c.removeRouteFromWireguardPeer(); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err))
|
||||||
|
}
|
||||||
|
if err := c.handler.RemoveRoute(); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove route: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
|
func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
|
||||||
routerPeerStatuses := c.getRouterPeerStatuses()
|
routerPeerStatuses := c.getRouterPeerStatuses()
|
||||||
|
|
||||||
chosen := c.getBestRouteFromStatuses(routerPeerStatuses)
|
newChosenID := c.getBestRouteFromStatuses(routerPeerStatuses)
|
||||||
|
|
||||||
// If no route is chosen, remove the route from the peer and system
|
// If no route is chosen, remove the route from the peer and system
|
||||||
if chosen == "" {
|
if newChosenID == "" {
|
||||||
if err := c.removeRouteFromPeerAndSystem(); err != nil {
|
if err := c.removeRouteFromPeerAndSystem(); err != nil {
|
||||||
return fmt.Errorf("remove route from peer and system: %v", err)
|
return fmt.Errorf("remove route for peer %s: %w", c.currentChosen.Peer, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.chosenRoute = nil
|
c.currentChosen = nil
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the chosen route is the same as the current route, do nothing
|
// If the chosen route is the same as the current route, do nothing
|
||||||
if c.chosenRoute != nil && c.chosenRoute.ID == chosen {
|
if c.currentChosen != nil && c.currentChosen.ID == newChosenID &&
|
||||||
if c.chosenRoute.IsEqual(c.routes[chosen]) {
|
c.currentChosen.IsEqual(c.routes[newChosenID]) {
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.chosenRoute != nil {
|
if c.currentChosen == nil {
|
||||||
// If a previous route exists, remove it from the peer
|
// If they were not previously assigned to another peer, add routes to the system first
|
||||||
if err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer); err != nil {
|
if err := c.handler.AddRoute(c.ctx); err != nil {
|
||||||
return fmt.Errorf("remove route from peer: %v", err)
|
return fmt.Errorf("add route: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// otherwise add the route to the system
|
// Otherwise, remove the allowed IPs from the previous peer first
|
||||||
if err := addVPNRoute(c.network, c.wgInterface.Name()); err != nil {
|
if err := c.removeRouteFromWireguardPeer(); err != nil {
|
||||||
return fmt.Errorf("route %s couldn't be added for peer %s, err: %v",
|
return fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err)
|
||||||
c.network.String(), c.wgInterface.Address().IP.String(), err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.chosenRoute = c.routes[chosen]
|
c.currentChosen = c.routes[newChosenID]
|
||||||
|
|
||||||
state, err := c.statusRecorder.GetPeer(c.chosenRoute.Peer)
|
if err := c.handler.AddAllowedIPs(c.currentChosen.Peer); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("add allowed IPs for peer %s: %w", c.currentChosen.Peer, err)
|
||||||
log.Errorf("Failed to get peer state: %v", err)
|
|
||||||
} else {
|
|
||||||
state.AddRoute(c.network.String())
|
|
||||||
if err := c.statusRecorder.UpdatePeerState(state); err != nil {
|
|
||||||
log.Warnf("Failed to update peer state: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.wgInterface.AddAllowedIP(c.chosenRoute.Peer, c.network.String()); err != nil {
|
c.addStateRoute()
|
||||||
log.Errorf("couldn't add allowed IP %s added for peer %s, err: %v",
|
|
||||||
c.network, c.chosenRoute.Peer, 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
|
||||||
@@ -289,7 +310,7 @@ func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *clientNetwork) handleUpdate(update routesUpdate) {
|
func (c *clientNetwork) handleUpdate(update routesUpdate) {
|
||||||
updateMap := make(map[string]*route.Route)
|
updateMap := make(map[route.ID]*route.Route)
|
||||||
|
|
||||||
for _, r := range update.routes {
|
for _, r := range update.routes {
|
||||||
updateMap[r.ID] = r
|
updateMap[r.ID] = r
|
||||||
@@ -312,24 +333,23 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-c.ctx.Done():
|
case <-c.ctx.Done():
|
||||||
log.Debugf("stopping watcher for network %s", c.network)
|
log.Debugf("Stopping watcher for network [%v]", c.handler)
|
||||||
err := c.removeRouteFromPeerAndSystem()
|
if err := c.removeRouteFromPeerAndSystem(); err != nil {
|
||||||
if err != nil {
|
log.Errorf("Failed to remove routes for [%v]: %v", c.handler, err)
|
||||||
log.Errorf("Couldn't remove route from peer and system for network %s: %v", c.network, err)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case <-c.peerStateUpdate:
|
case <-c.peerStateUpdate:
|
||||||
err := c.recalculateRouteAndUpdatePeerAndSystem()
|
err := c.recalculateRouteAndUpdatePeerAndSystem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Couldn't recalculate route and update peer and system: %v", err)
|
log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err)
|
||||||
}
|
}
|
||||||
case update := <-c.routeUpdate:
|
case update := <-c.routeUpdate:
|
||||||
if update.updateSerial < c.updateSerial {
|
if update.updateSerial < c.updateSerial {
|
||||||
log.Warnf("Received a routes update with smaller serial number, ignoring it")
|
log.Warnf("Received a routes update with smaller serial number (%d -> %d), ignoring it", c.updateSerial, update.updateSerial)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Received a new client network route update for %s", c.network)
|
log.Debugf("Received a new client network route update for [%v]", c.handler)
|
||||||
|
|
||||||
c.handleUpdate(update)
|
c.handleUpdate(update)
|
||||||
|
|
||||||
@@ -337,10 +357,17 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() {
|
|||||||
|
|
||||||
err := c.recalculateRouteAndUpdatePeerAndSystem()
|
err := c.recalculateRouteAndUpdatePeerAndSystem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Couldn't recalculate route and update peer and system for network %s: %v", c.network, err)
|
log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.startPeersStatusChangeWatcher()
|
c.startPeersStatusChangeWatcher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handlerFromRoute(rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, dnsRouterInteval time.Duration, statusRecorder *peer.Status) RouteHandler {
|
||||||
|
if rt.IsDynamic() {
|
||||||
|
return dynamic.NewRoute(rt, routeRefCounter, allowedIPsRefCounter, dnsRouterInteval, statusRecorder)
|
||||||
|
}
|
||||||
|
return static.NewRoute(rt, routeRefCounter, allowedIPsRefCounter)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/static"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,21 +13,21 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
statuses map[string]routerPeerStatus
|
statuses map[route.ID]routerPeerStatus
|
||||||
expectedRouteID string
|
expectedRouteID route.ID
|
||||||
currentRoute string
|
currentRoute route.ID
|
||||||
existingRoutes map[string]*route.Route
|
existingRoutes map[route.ID]*route.Route
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "one route",
|
name: "one route",
|
||||||
statuses: map[string]routerPeerStatus{
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
"route1": {
|
"route1": {
|
||||||
connected: true,
|
connected: true,
|
||||||
relayed: false,
|
relayed: false,
|
||||||
direct: true,
|
direct: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
existingRoutes: map[string]*route.Route{
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
Metric: route.MaxMetric,
|
Metric: route.MaxMetric,
|
||||||
@@ -38,14 +39,14 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "one connected routes with relayed and direct",
|
name: "one connected routes with relayed and direct",
|
||||||
statuses: map[string]routerPeerStatus{
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
"route1": {
|
"route1": {
|
||||||
connected: true,
|
connected: true,
|
||||||
relayed: true,
|
relayed: true,
|
||||||
direct: true,
|
direct: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
existingRoutes: map[string]*route.Route{
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
Metric: route.MaxMetric,
|
Metric: route.MaxMetric,
|
||||||
@@ -57,14 +58,14 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "one connected routes with relayed and no direct",
|
name: "one connected routes with relayed and no direct",
|
||||||
statuses: map[string]routerPeerStatus{
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
"route1": {
|
"route1": {
|
||||||
connected: true,
|
connected: true,
|
||||||
relayed: true,
|
relayed: true,
|
||||||
direct: false,
|
direct: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
existingRoutes: map[string]*route.Route{
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
Metric: route.MaxMetric,
|
Metric: route.MaxMetric,
|
||||||
@@ -76,14 +77,14 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no connected peers",
|
name: "no connected peers",
|
||||||
statuses: map[string]routerPeerStatus{
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
"route1": {
|
"route1": {
|
||||||
connected: false,
|
connected: false,
|
||||||
relayed: false,
|
relayed: false,
|
||||||
direct: false,
|
direct: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
existingRoutes: map[string]*route.Route{
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
Metric: route.MaxMetric,
|
Metric: route.MaxMetric,
|
||||||
@@ -95,7 +96,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple connected peers with different metrics",
|
name: "multiple connected peers with different metrics",
|
||||||
statuses: map[string]routerPeerStatus{
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
"route1": {
|
"route1": {
|
||||||
connected: true,
|
connected: true,
|
||||||
relayed: false,
|
relayed: false,
|
||||||
@@ -107,7 +108,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
direct: true,
|
direct: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
existingRoutes: map[string]*route.Route{
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
Metric: 9000,
|
Metric: 9000,
|
||||||
@@ -124,7 +125,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple connected peers with one relayed",
|
name: "multiple connected peers with one relayed",
|
||||||
statuses: map[string]routerPeerStatus{
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
"route1": {
|
"route1": {
|
||||||
connected: true,
|
connected: true,
|
||||||
relayed: false,
|
relayed: false,
|
||||||
@@ -136,7 +137,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
direct: true,
|
direct: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
existingRoutes: map[string]*route.Route{
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
Metric: route.MaxMetric,
|
Metric: route.MaxMetric,
|
||||||
@@ -153,7 +154,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple connected peers with one direct",
|
name: "multiple connected peers with one direct",
|
||||||
statuses: map[string]routerPeerStatus{
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
"route1": {
|
"route1": {
|
||||||
connected: true,
|
connected: true,
|
||||||
relayed: false,
|
relayed: false,
|
||||||
@@ -165,7 +166,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
direct: false,
|
direct: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
existingRoutes: map[string]*route.Route{
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
Metric: route.MaxMetric,
|
Metric: route.MaxMetric,
|
||||||
@@ -182,7 +183,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple connected peers with different latencies",
|
name: "multiple connected peers with different latencies",
|
||||||
statuses: map[string]routerPeerStatus{
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
"route1": {
|
"route1": {
|
||||||
connected: true,
|
connected: true,
|
||||||
latency: 300 * time.Millisecond,
|
latency: 300 * time.Millisecond,
|
||||||
@@ -192,7 +193,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
latency: 10 * time.Millisecond,
|
latency: 10 * time.Millisecond,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
existingRoutes: map[string]*route.Route{
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
Metric: route.MaxMetric,
|
Metric: route.MaxMetric,
|
||||||
@@ -209,7 +210,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "should ignore routes with latency 0",
|
name: "should ignore routes with latency 0",
|
||||||
statuses: map[string]routerPeerStatus{
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
"route1": {
|
"route1": {
|
||||||
connected: true,
|
connected: true,
|
||||||
latency: 0 * time.Millisecond,
|
latency: 0 * time.Millisecond,
|
||||||
@@ -219,7 +220,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
latency: 10 * time.Millisecond,
|
latency: 10 * time.Millisecond,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
existingRoutes: map[string]*route.Route{
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
Metric: route.MaxMetric,
|
Metric: route.MaxMetric,
|
||||||
@@ -236,12 +237,12 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "current route with similar score and similar but slightly worse latency should not change",
|
name: "current route with similar score and similar but slightly worse latency should not change",
|
||||||
statuses: map[string]routerPeerStatus{
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
"route1": {
|
"route1": {
|
||||||
connected: true,
|
connected: true,
|
||||||
relayed: false,
|
relayed: false,
|
||||||
direct: true,
|
direct: true,
|
||||||
latency: 12 * time.Millisecond,
|
latency: 15 * time.Millisecond,
|
||||||
},
|
},
|
||||||
"route2": {
|
"route2": {
|
||||||
connected: true,
|
connected: true,
|
||||||
@@ -250,7 +251,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
latency: 10 * time.Millisecond,
|
latency: 10 * time.Millisecond,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
existingRoutes: map[string]*route.Route{
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
Metric: route.MaxMetric,
|
Metric: route.MaxMetric,
|
||||||
@@ -265,9 +266,40 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
currentRoute: "route1",
|
currentRoute: "route1",
|
||||||
expectedRouteID: "route1",
|
expectedRouteID: "route1",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "current route with bad score should be changed to route with better score",
|
||||||
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
|
"route1": {
|
||||||
|
connected: true,
|
||||||
|
relayed: false,
|
||||||
|
direct: true,
|
||||||
|
latency: 200 * time.Millisecond,
|
||||||
|
},
|
||||||
|
"route2": {
|
||||||
|
connected: true,
|
||||||
|
relayed: false,
|
||||||
|
direct: true,
|
||||||
|
latency: 10 * 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: "route2",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "current chosen route doesn't exist anymore",
|
name: "current chosen route doesn't exist anymore",
|
||||||
statuses: map[string]routerPeerStatus{
|
statuses: map[route.ID]routerPeerStatus{
|
||||||
"route1": {
|
"route1": {
|
||||||
connected: true,
|
connected: true,
|
||||||
relayed: false,
|
relayed: false,
|
||||||
@@ -281,7 +313,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
latency: 10 * time.Millisecond,
|
latency: 10 * time.Millisecond,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
existingRoutes: map[string]*route.Route{
|
existingRoutes: map[route.ID]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
Metric: route.MaxMetric,
|
Metric: route.MaxMetric,
|
||||||
@@ -309,9 +341,9 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
|
|||||||
|
|
||||||
// create new clientNetwork
|
// create new clientNetwork
|
||||||
client := &clientNetwork{
|
client := &clientNetwork{
|
||||||
network: netip.MustParsePrefix("192.168.0.0/24"),
|
handler: static.NewRoute(&route.Route{Network: netip.MustParsePrefix("192.168.0.0/24")}, nil, nil),
|
||||||
routes: tc.existingRoutes,
|
routes: tc.existingRoutes,
|
||||||
chosenRoute: currentRoute,
|
currentChosen: currentRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
chosenRoute := client.getBestRouteFromStatuses(tc.statuses)
|
chosenRoute := client.getBestRouteFromStatuses(tc.statuses)
|
||||||
|
|||||||
378
client/internal/routemanager/dynamic/route.go
Normal file
378
client/internal/routemanager/dynamic/route.go
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
package dynamic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/util"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultInterval = time.Minute
|
||||||
|
|
||||||
|
minInterval = 2 * time.Second
|
||||||
|
failureInterval = 5 * time.Second
|
||||||
|
|
||||||
|
addAllowedIP = "add allowed IP %s: %w"
|
||||||
|
)
|
||||||
|
|
||||||
|
type domainMap map[domain.Domain][]netip.Prefix
|
||||||
|
|
||||||
|
type resolveResult struct {
|
||||||
|
domain domain.Domain
|
||||||
|
prefix netip.Prefix
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Route struct {
|
||||||
|
route *route.Route
|
||||||
|
routeRefCounter *refcounter.RouteRefCounter
|
||||||
|
allowedIPsRefcounter *refcounter.AllowedIPsRefCounter
|
||||||
|
interval time.Duration
|
||||||
|
dynamicDomains domainMap
|
||||||
|
mu sync.Mutex
|
||||||
|
currentPeerKey string
|
||||||
|
cancel context.CancelFunc
|
||||||
|
statusRecorder *peer.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRoute(
|
||||||
|
rt *route.Route,
|
||||||
|
routeRefCounter *refcounter.RouteRefCounter,
|
||||||
|
allowedIPsRefCounter *refcounter.AllowedIPsRefCounter,
|
||||||
|
interval time.Duration,
|
||||||
|
statusRecorder *peer.Status,
|
||||||
|
) *Route {
|
||||||
|
return &Route{
|
||||||
|
route: rt,
|
||||||
|
routeRefCounter: routeRefCounter,
|
||||||
|
allowedIPsRefcounter: allowedIPsRefCounter,
|
||||||
|
interval: interval,
|
||||||
|
dynamicDomains: domainMap{},
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) String() string {
|
||||||
|
s, err := r.route.Domains.String()
|
||||||
|
if err != nil {
|
||||||
|
return r.route.Domains.PunycodeString()
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) AddRoute(ctx context.Context) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
if r.cancel != nil {
|
||||||
|
r.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, r.cancel = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
go r.startResolver(ctx)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRoute will stop the dynamic resolver and remove all dynamic routes.
|
||||||
|
// It doesn't touch allowed IPs, these should be removed separately and before calling this method.
|
||||||
|
func (r *Route) RemoveRoute() error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
if r.cancel != nil {
|
||||||
|
r.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
for domain, prefixes := range r.dynamicDomains {
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if _, err := r.routeRefCounter.Decrement(prefix); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove dynamic route for IP %s: %w", prefix, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("Removed dynamic route(s) for [%s]: %s", domain.SafeString(), strings.ReplaceAll(fmt.Sprintf("%s", prefixes), " ", ", "))
|
||||||
|
|
||||||
|
r.statusRecorder.DeleteResolvedDomainsStates(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.dynamicDomains = domainMap{}
|
||||||
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) AddAllowedIPs(peerKey string) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
for domain, domainPrefixes := range r.dynamicDomains {
|
||||||
|
for _, prefix := range domainPrefixes {
|
||||||
|
if err := r.incrementAllowedIP(domain, prefix, peerKey); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf(addAllowedIP, prefix, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.currentPeerKey = peerKey
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) RemoveAllowedIPs() error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
for _, domainPrefixes := range r.dynamicDomains {
|
||||||
|
for _, prefix := range domainPrefixes {
|
||||||
|
if _, err := r.allowedIPsRefcounter.Decrement(prefix); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove allowed IP %s: %w", prefix, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.currentPeerKey = ""
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) startResolver(ctx context.Context) {
|
||||||
|
log.Debugf("Starting dynamic route resolver for domains [%v]", r)
|
||||||
|
|
||||||
|
interval := r.interval
|
||||||
|
if interval < minInterval {
|
||||||
|
interval = minInterval
|
||||||
|
log.Warnf("Dynamic route resolver interval %s is too low, setting to minimum value %s", r.interval, minInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
if err := r.update(ctx); err != nil {
|
||||||
|
log.Errorf("Failed to resolve domains for route [%v]: %v", r, err)
|
||||||
|
if interval > failureInterval {
|
||||||
|
ticker.Reset(failureInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Debugf("Stopping dynamic route resolver for domains [%v]", r)
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := r.update(ctx); err != nil {
|
||||||
|
log.Errorf("Failed to resolve domains for route [%v]: %v", r, err)
|
||||||
|
// Use a lower ticker interval if the update fails
|
||||||
|
if interval > failureInterval {
|
||||||
|
ticker.Reset(failureInterval)
|
||||||
|
}
|
||||||
|
} else if interval > failureInterval {
|
||||||
|
// Reset to the original interval if the update succeeds
|
||||||
|
ticker.Reset(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) update(ctx context.Context) error {
|
||||||
|
if resolved, err := r.resolveDomains(); err != nil {
|
||||||
|
return fmt.Errorf("resolve domains: %w", err)
|
||||||
|
} else if err := r.updateDynamicRoutes(ctx, resolved); err != nil {
|
||||||
|
return fmt.Errorf("update dynamic routes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) resolveDomains() (domainMap, error) {
|
||||||
|
results := make(chan resolveResult)
|
||||||
|
go r.resolve(results)
|
||||||
|
|
||||||
|
resolved := domainMap{}
|
||||||
|
var merr *multierror.Error
|
||||||
|
|
||||||
|
for result := range results {
|
||||||
|
if result.err != nil {
|
||||||
|
merr = multierror.Append(merr, result.err)
|
||||||
|
} else {
|
||||||
|
resolved[result.domain] = append(resolved[result.domain], result.prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved, nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) resolve(results chan resolveResult) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for _, d := range r.route.Domains {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(domain domain.Domain) {
|
||||||
|
defer wg.Done()
|
||||||
|
ips, err := net.LookupIP(string(domain))
|
||||||
|
if err != nil {
|
||||||
|
results <- resolveResult{domain: domain, err: fmt.Errorf("resolve d %s: %w", domain.SafeString(), err)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, ip := range ips {
|
||||||
|
prefix, err := util.GetPrefixFromIP(ip)
|
||||||
|
if err != nil {
|
||||||
|
results <- resolveResult{domain: domain, err: fmt.Errorf("get prefix from IP %s: %w", ip.String(), err)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
results <- resolveResult{domain: domain, prefix: prefix}
|
||||||
|
}
|
||||||
|
}(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) updateDynamicRoutes(ctx context.Context, newDomains domainMap) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
|
||||||
|
for domain, newPrefixes := range newDomains {
|
||||||
|
oldPrefixes := r.dynamicDomains[domain]
|
||||||
|
toAdd, toRemove := determinePrefixChanges(oldPrefixes, newPrefixes)
|
||||||
|
|
||||||
|
addedPrefixes, err := r.addRoutes(domain, toAdd)
|
||||||
|
if err != nil {
|
||||||
|
merr = multierror.Append(merr, err)
|
||||||
|
} else if len(addedPrefixes) > 0 {
|
||||||
|
log.Debugf("Added dynamic route(s) for [%s]: %s", domain.SafeString(), strings.ReplaceAll(fmt.Sprintf("%s", addedPrefixes), " ", ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
removedPrefixes, err := r.removeRoutes(toRemove)
|
||||||
|
if err != nil {
|
||||||
|
merr = multierror.Append(merr, err)
|
||||||
|
} else if len(removedPrefixes) > 0 {
|
||||||
|
log.Debugf("Removed dynamic route(s) for [%s]: %s", domain.SafeString(), strings.ReplaceAll(fmt.Sprintf("%s", removedPrefixes), " ", ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedPrefixes := combinePrefixes(oldPrefixes, removedPrefixes, addedPrefixes)
|
||||||
|
r.dynamicDomains[domain] = updatedPrefixes
|
||||||
|
|
||||||
|
r.statusRecorder.UpdateResolvedDomainsStates(domain, updatedPrefixes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) addRoutes(domain domain.Domain, prefixes []netip.Prefix) ([]netip.Prefix, error) {
|
||||||
|
var addedPrefixes []netip.Prefix
|
||||||
|
var merr *multierror.Error
|
||||||
|
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if _, err := r.routeRefCounter.Increment(prefix, nil); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("add dynamic route for IP %s: %w", prefix, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r.currentPeerKey != "" {
|
||||||
|
if err := r.incrementAllowedIP(domain, prefix, r.currentPeerKey); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf(addAllowedIP, prefix, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addedPrefixes = append(addedPrefixes, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
return addedPrefixes, merr.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) removeRoutes(prefixes []netip.Prefix) ([]netip.Prefix, error) {
|
||||||
|
if r.route.KeepRoute {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var removedPrefixes []netip.Prefix
|
||||||
|
var merr *multierror.Error
|
||||||
|
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if _, err := r.routeRefCounter.Decrement(prefix); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove dynamic route for IP %s: %w", prefix, err))
|
||||||
|
}
|
||||||
|
if r.currentPeerKey != "" {
|
||||||
|
if _, err := r.allowedIPsRefcounter.Decrement(prefix); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove allowed IP %s: %w", prefix, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removedPrefixes = append(removedPrefixes, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
return removedPrefixes, merr.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) incrementAllowedIP(domain domain.Domain, prefix netip.Prefix, peerKey string) error {
|
||||||
|
if ref, err := r.allowedIPsRefcounter.Increment(prefix, peerKey); err != nil {
|
||||||
|
return fmt.Errorf(addAllowedIP, 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func combinePrefixes(oldPrefixes, removedPrefixes, addedPrefixes []netip.Prefix) []netip.Prefix {
|
||||||
|
prefixSet := make(map[netip.Prefix]struct{})
|
||||||
|
for _, prefix := range oldPrefixes {
|
||||||
|
prefixSet[prefix] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, prefix := range removedPrefixes {
|
||||||
|
delete(prefixSet, prefix)
|
||||||
|
}
|
||||||
|
for _, prefix := range addedPrefixes {
|
||||||
|
prefixSet[prefix] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var combinedPrefixes []netip.Prefix
|
||||||
|
for prefix := range prefixSet {
|
||||||
|
combinedPrefixes = append(combinedPrefixes, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinedPrefixes
|
||||||
|
}
|
||||||
@@ -2,33 +2,36 @@ package routemanager
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"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/routemanager/refcounter"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routeselector"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultv4 = netip.PrefixFrom(netip.IPv4Unspecified(), 0)
|
|
||||||
|
|
||||||
// nolint:unused
|
|
||||||
var defaultv6 = netip.PrefixFrom(netip.IPv6Unspecified(), 0)
|
|
||||||
|
|
||||||
// Manager is a route manager interface
|
// Manager is a route manager interface
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error)
|
Init() (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error)
|
||||||
UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error
|
UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, error)
|
||||||
|
TriggerSelection(route.HAMap)
|
||||||
|
GetRouteSelector() *routeselector.RouteSelector
|
||||||
SetRouteChangeListener(listener listener.NetworkChangeListener)
|
SetRouteChangeListener(listener listener.NetworkChangeListener)
|
||||||
InitialRouteRange() []string
|
InitialRouteRange() []string
|
||||||
EnableServerRouter(firewall firewall.Manager) error
|
EnableServerRouter(firewall firewall.Manager) error
|
||||||
@@ -37,29 +40,71 @@ type Manager interface {
|
|||||||
|
|
||||||
// DefaultManager is the default instance of a route manager
|
// DefaultManager is the default instance of a route manager
|
||||||
type DefaultManager struct {
|
type DefaultManager struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
stop context.CancelFunc
|
stop context.CancelFunc
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
clientNetworks map[string]*clientNetwork
|
clientNetworks map[route.HAUniqueID]*clientNetwork
|
||||||
serverRouter serverRouter
|
routeSelector *routeselector.RouteSelector
|
||||||
statusRecorder *peer.Status
|
serverRouter serverRouter
|
||||||
wgInterface *iface.WGIface
|
sysOps *systemops.SysOps
|
||||||
pubKey string
|
statusRecorder *peer.Status
|
||||||
notifier *notifier
|
wgInterface iface.IWGIface
|
||||||
|
pubKey string
|
||||||
|
notifier *notifier
|
||||||
|
routeRefCounter *refcounter.RouteRefCounter
|
||||||
|
allowedIPsRefCounter *refcounter.AllowedIPsRefCounter
|
||||||
|
dnsRouteInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, statusRecorder *peer.Status, initialRoutes []*route.Route) *DefaultManager {
|
func NewManager(
|
||||||
|
ctx context.Context,
|
||||||
|
pubKey string,
|
||||||
|
dnsRouteInterval time.Duration,
|
||||||
|
wgInterface iface.IWGIface,
|
||||||
|
statusRecorder *peer.Status,
|
||||||
|
initialRoutes []*route.Route,
|
||||||
|
) *DefaultManager {
|
||||||
mCTX, cancel := context.WithCancel(ctx)
|
mCTX, cancel := context.WithCancel(ctx)
|
||||||
|
sysOps := systemops.NewSysOps(wgInterface)
|
||||||
|
|
||||||
dm := &DefaultManager{
|
dm := &DefaultManager{
|
||||||
ctx: mCTX,
|
ctx: mCTX,
|
||||||
stop: cancel,
|
stop: cancel,
|
||||||
clientNetworks: make(map[string]*clientNetwork),
|
dnsRouteInterval: dnsRouteInterval,
|
||||||
statusRecorder: statusRecorder,
|
clientNetworks: make(map[route.HAUniqueID]*clientNetwork),
|
||||||
wgInterface: wgInterface,
|
routeSelector: routeselector.NewRouteSelector(),
|
||||||
pubKey: pubKey,
|
sysOps: sysOps,
|
||||||
notifier: newNotifier(),
|
statusRecorder: statusRecorder,
|
||||||
|
wgInterface: wgInterface,
|
||||||
|
pubKey: pubKey,
|
||||||
|
notifier: newNotifier(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dm.routeRefCounter = refcounter.New(
|
||||||
|
func(prefix netip.Prefix, _ any) (any, error) {
|
||||||
|
return nil, sysOps.AddVPNRoute(prefix, wgInterface.ToInterface())
|
||||||
|
},
|
||||||
|
func(prefix netip.Prefix, _ any) error {
|
||||||
|
return sysOps.RemoveVPNRoute(prefix, wgInterface.ToInterface())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
dm.allowedIPsRefCounter = refcounter.New(
|
||||||
|
func(prefix netip.Prefix, peerKey string) (string, error) {
|
||||||
|
// save peerKey to use it in the remove function
|
||||||
|
return peerKey, wgInterface.AddAllowedIP(peerKey, prefix.String())
|
||||||
|
},
|
||||||
|
func(prefix netip.Prefix, peerKey string) error {
|
||||||
|
if err := wgInterface.RemoveAllowedIP(peerKey, prefix.String()); err != nil {
|
||||||
|
if !errors.Is(err, iface.ErrPeerNotFound) && !errors.Is(err, iface.ErrAllowedIPNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Tracef("Remove allowed IPs %s for %s: %v", prefix, peerKey, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if runtime.GOOS == "android" {
|
if runtime.GOOS == "android" {
|
||||||
cr := dm.clientRoutes(initialRoutes)
|
cr := dm.clientRoutes(initialRoutes)
|
||||||
dm.notifier.setInitialClientRoutes(cr)
|
dm.notifier.setInitialClientRoutes(cr)
|
||||||
@@ -68,12 +113,12 @@ func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init sets up the routing
|
// Init sets up the routing
|
||||||
func (m *DefaultManager) Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) {
|
func (m *DefaultManager) Init() (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) {
|
||||||
if nbnet.CustomRoutingDisabled() {
|
if nbnet.CustomRoutingDisabled() {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cleanupRouting(); err != nil {
|
if err := m.sysOps.CleanupRouting(); err != nil {
|
||||||
log.Warnf("Failed cleaning up routing: %v", err)
|
log.Warnf("Failed cleaning up routing: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +126,7 @@ func (m *DefaultManager) Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePee
|
|||||||
signalAddress := m.statusRecorder.GetSignalState().URL
|
signalAddress := m.statusRecorder.GetSignalState().URL
|
||||||
ips := resolveURLsToIPs([]string{mgmtAddress, signalAddress})
|
ips := resolveURLsToIPs([]string{mgmtAddress, signalAddress})
|
||||||
|
|
||||||
beforePeerHook, afterPeerHook, err := setupRouting(ips, m.wgInterface)
|
beforePeerHook, afterPeerHook, err := m.sysOps.SetupRouting(ips)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("setup routing: %w", err)
|
return nil, nil, fmt.Errorf("setup routing: %w", err)
|
||||||
}
|
}
|
||||||
@@ -105,8 +150,19 @@ func (m *DefaultManager) Stop() {
|
|||||||
m.serverRouter.cleanUp()
|
m.serverRouter.cleanUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.routeRefCounter != nil {
|
||||||
|
if err := m.routeRefCounter.Flush(); err != nil {
|
||||||
|
log.Errorf("Error flushing route ref counter: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.allowedIPsRefCounter != nil {
|
||||||
|
if err := m.allowedIPsRefCounter.Flush(); err != nil {
|
||||||
|
log.Errorf("Error flushing allowed IPs ref counter: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !nbnet.CustomRoutingDisabled() {
|
if !nbnet.CustomRoutingDisabled() {
|
||||||
if err := cleanupRouting(); err != nil {
|
if err := m.sysOps.CleanupRouting(); err != nil {
|
||||||
log.Errorf("Error cleaning up routing: %v", err)
|
log.Errorf("Error cleaning up routing: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Info("Routing cleanup complete")
|
log.Info("Routing cleanup complete")
|
||||||
@@ -117,28 +173,29 @@ func (m *DefaultManager) Stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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) error {
|
func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, 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 m.ctx.Err()
|
return nil, nil, m.ctx.Err()
|
||||||
default:
|
default:
|
||||||
m.mux.Lock()
|
m.mux.Lock()
|
||||||
defer m.mux.Unlock()
|
defer m.mux.Unlock()
|
||||||
|
|
||||||
newServerRoutesMap, newClientRoutesIDMap := m.classifiesRoutes(newRoutes)
|
newServerRoutesMap, newClientRoutesIDMap := m.classifyRoutes(newRoutes)
|
||||||
|
|
||||||
m.updateClientNetworks(updateSerial, newClientRoutesIDMap)
|
filteredClientRoutes := m.routeSelector.FilterSelected(newClientRoutesIDMap)
|
||||||
m.notifier.onNewRoutes(newClientRoutesIDMap)
|
m.updateClientNetworks(updateSerial, filteredClientRoutes)
|
||||||
|
m.notifier.onNewRoutes(filteredClientRoutes)
|
||||||
|
|
||||||
if m.serverRouter != nil {
|
if m.serverRouter != nil {
|
||||||
err := m.serverRouter.updateRoutes(newServerRoutesMap)
|
err := m.serverRouter.updateRoutes(newServerRoutesMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("update routes: %w", err)
|
return nil, nil, fmt.Errorf("update routes: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return newServerRoutesMap, newClientRoutesIDMap, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,24 +206,62 @@ func (m *DefaultManager) SetRouteChangeListener(listener listener.NetworkChangeL
|
|||||||
|
|
||||||
// InitialRouteRange return the list of initial routes. It used by mobile systems
|
// InitialRouteRange return the list of initial routes. It used by mobile systems
|
||||||
func (m *DefaultManager) InitialRouteRange() []string {
|
func (m *DefaultManager) InitialRouteRange() []string {
|
||||||
return m.notifier.initialRouteRanges()
|
return m.notifier.getInitialRouteRanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks map[string][]*route.Route) {
|
// GetRouteSelector returns the route selector
|
||||||
// removing routes that do not exist as per the update from the Management service.
|
func (m *DefaultManager) GetRouteSelector() *routeselector.RouteSelector {
|
||||||
|
return m.routeSelector
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientRoutes returns the client routes
|
||||||
|
func (m *DefaultManager) GetClientRoutes() map[route.HAUniqueID]*clientNetwork {
|
||||||
|
return m.clientNetworks
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerSelection triggers the selection of routes, stopping deselected watchers and starting newly selected ones
|
||||||
|
func (m *DefaultManager) TriggerSelection(networks route.HAMap) {
|
||||||
|
m.mux.Lock()
|
||||||
|
defer m.mux.Unlock()
|
||||||
|
|
||||||
|
networks = m.routeSelector.FilterSelected(networks)
|
||||||
|
|
||||||
|
m.notifier.onNewRoutes(networks)
|
||||||
|
|
||||||
|
m.stopObsoleteClients(networks)
|
||||||
|
|
||||||
|
for id, routes := range networks {
|
||||||
|
if _, found := m.clientNetworks[id]; found {
|
||||||
|
// don't touch existing client network watchers
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
clientNetworkWatcher := newClientNetworkWatcher(m.ctx, m.dnsRouteInterval, m.wgInterface, m.statusRecorder, routes[0], m.routeRefCounter, m.allowedIPsRefCounter)
|
||||||
|
m.clientNetworks[id] = clientNetworkWatcher
|
||||||
|
go clientNetworkWatcher.peersStateAndUpdateWatcher()
|
||||||
|
clientNetworkWatcher.sendUpdateToClientNetworkWatcher(routesUpdate{routes: routes})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopObsoleteClients stops the client network watcher for the networks that are not in the new list
|
||||||
|
func (m *DefaultManager) stopObsoleteClients(networks route.HAMap) {
|
||||||
for id, client := range m.clientNetworks {
|
for id, client := range m.clientNetworks {
|
||||||
_, found := networks[id]
|
if _, ok := networks[id]; !ok {
|
||||||
if !found {
|
log.Debugf("Stopping client network watcher, %s", id)
|
||||||
log.Debugf("stopping client network watcher, %s", id)
|
client.cancel()
|
||||||
client.stop()
|
|
||||||
delete(m.clientNetworks, id)
|
delete(m.clientNetworks, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks route.HAMap) {
|
||||||
|
// removing routes that do not exist as per the update from the Management service.
|
||||||
|
m.stopObsoleteClients(networks)
|
||||||
|
|
||||||
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.wgInterface, m.statusRecorder, routes[0].Network)
|
clientNetworkWatcher = newClientNetworkWatcher(m.ctx, m.dnsRouteInterval, m.wgInterface, m.statusRecorder, routes[0], m.routeRefCounter, m.allowedIPsRefCounter)
|
||||||
m.clientNetworks[id] = clientNetworkWatcher
|
m.clientNetworks[id] = clientNetworkWatcher
|
||||||
go clientNetworkWatcher.peersStateAndUpdateWatcher()
|
go clientNetworkWatcher.peersStateAndUpdateWatcher()
|
||||||
}
|
}
|
||||||
@@ -178,15 +273,15 @@ func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks map[
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DefaultManager) classifiesRoutes(newRoutes []*route.Route) (map[string]*route.Route, map[string][]*route.Route) {
|
func (m *DefaultManager) classifyRoutes(newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap) {
|
||||||
newClientRoutesIDMap := make(map[string][]*route.Route)
|
newClientRoutesIDMap := make(route.HAMap)
|
||||||
newServerRoutesMap := make(map[string]*route.Route)
|
newServerRoutesMap := make(map[route.ID]*route.Route)
|
||||||
ownNetworkIDs := make(map[string]bool)
|
ownNetworkIDs := make(map[route.HAUniqueID]bool)
|
||||||
|
|
||||||
for _, newRoute := range newRoutes {
|
for _, newRoute := range newRoutes {
|
||||||
networkID := route.GetHAUniqueID(newRoute)
|
haID := newRoute.GetHAUniqueID()
|
||||||
if newRoute.Peer == m.pubKey {
|
if newRoute.Peer == m.pubKey {
|
||||||
ownNetworkIDs[networkID] = true
|
ownNetworkIDs[haID] = true
|
||||||
// only linux is supported for now
|
// only linux is supported for now
|
||||||
if runtime.GOOS != "linux" {
|
if runtime.GOOS != "linux" {
|
||||||
log.Warnf("received a route to manage, but agent doesn't support router mode on %s OS", runtime.GOOS)
|
log.Warnf("received a route to manage, but agent doesn't support router mode on %s OS", runtime.GOOS)
|
||||||
@@ -197,12 +292,12 @@ func (m *DefaultManager) classifiesRoutes(newRoutes []*route.Route) (map[string]
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, newRoute := range newRoutes {
|
for _, newRoute := range newRoutes {
|
||||||
networkID := route.GetHAUniqueID(newRoute)
|
haID := newRoute.GetHAUniqueID()
|
||||||
if !ownNetworkIDs[networkID] {
|
if !ownNetworkIDs[haID] {
|
||||||
if !isPrefixSupported(newRoute.Network) {
|
if !isRouteSupported(newRoute) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
newClientRoutesIDMap[networkID] = append(newClientRoutesIDMap[networkID], newRoute)
|
newClientRoutesIDMap[haID] = append(newClientRoutesIDMap[haID], newRoute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,27 +305,24 @@ func (m *DefaultManager) classifiesRoutes(newRoutes []*route.Route) (map[string]
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *DefaultManager) clientRoutes(initialRoutes []*route.Route) []*route.Route {
|
func (m *DefaultManager) clientRoutes(initialRoutes []*route.Route) []*route.Route {
|
||||||
_, crMap := m.classifiesRoutes(initialRoutes)
|
_, crMap := m.classifyRoutes(initialRoutes)
|
||||||
rs := make([]*route.Route, 0)
|
rs := make([]*route.Route, 0, len(crMap))
|
||||||
for _, routes := range crMap {
|
for _, routes := range crMap {
|
||||||
rs = append(rs, routes...)
|
rs = append(rs, routes...)
|
||||||
}
|
}
|
||||||
return rs
|
return rs
|
||||||
}
|
}
|
||||||
|
|
||||||
func isPrefixSupported(prefix netip.Prefix) bool {
|
func isRouteSupported(route *route.Route) bool {
|
||||||
if !nbnet.CustomRoutingDisabled() {
|
if !nbnet.CustomRoutingDisabled() || route.IsDynamic() {
|
||||||
switch runtime.GOOS {
|
return true
|
||||||
case "linux", "windows", "darwin":
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If prefix is too small, lets assume it is a possible default prefix which is not yet supported
|
// If prefix is too small, lets assume it is a possible default prefix which is not yet supported
|
||||||
// we skip this prefix management
|
// we skip this prefix management
|
||||||
if prefix.Bits() <= minRangeBits {
|
if route.Network.Bits() <= vars.MinRangeBits {
|
||||||
log.Warnf("This agent version: %s, doesn't support default routes, received %s, skipping this prefix",
|
log.Warnf("This agent version: %s, doesn't support default routes, received %s, skipping this prefix",
|
||||||
version.NetbirdVersion(), prefix)
|
version.NetbirdVersion(), route.Network)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun43%d", n), "100.65.65.2/24", 33100, peerPrivateKey.String(), iface.DefaultMTU, newNet, nil)
|
wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun43%d", n), "100.65.65.2/24", 33100, peerPrivateKey.String(), iface.DefaultMTU, newNet, nil, nil)
|
||||||
require.NoError(t, err, "should create testing WGIface interface")
|
require.NoError(t, err, "should create testing WGIface interface")
|
||||||
defer wgInterface.Close()
|
defer wgInterface.Close()
|
||||||
|
|
||||||
@@ -416,7 +416,7 @@ 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, wgInterface, statusRecorder, nil)
|
routeManager := NewManager(ctx, localPeerKey, 0, wgInterface, statusRecorder, nil)
|
||||||
|
|
||||||
_, _, err = routeManager.Init()
|
_, _, err = routeManager.Init()
|
||||||
|
|
||||||
@@ -428,15 +428,15 @@ func TestManagerUpdateRoutes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(testCase.inputInitRoutes) > 0 {
|
if len(testCase.inputInitRoutes) > 0 {
|
||||||
err = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes)
|
_, _, err = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes)
|
||||||
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)
|
_, _, err = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), testCase.inputRoutes)
|
||||||
require.NoError(t, err, "should update routes")
|
require.NoError(t, err, "should update routes")
|
||||||
|
|
||||||
expectedWatchers := testCase.clientNetworkWatchersExpected
|
expectedWatchers := testCase.clientNetworkWatchersExpected
|
||||||
if (runtime.GOOS == "linux" || runtime.GOOS == "windows" || runtime.GOOS == "darwin") && testCase.clientNetworkWatchersExpectedAllowed != 0 {
|
if testCase.clientNetworkWatchersExpectedAllowed != 0 {
|
||||||
expectedWatchers = testCase.clientNetworkWatchersExpectedAllowed
|
expectedWatchers = testCase.clientNetworkWatchersExpectedAllowed
|
||||||
}
|
}
|
||||||
require.Len(t, routeManager.clientNetworks, expectedWatchers, "client networks size should match")
|
require.Len(t, routeManager.clientNetworks, expectedWatchers, "client networks size should match")
|
||||||
|
|||||||
@@ -6,18 +6,21 @@ import (
|
|||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"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/routeselector"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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) error
|
UpdateRoutesFunc func(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, error)
|
||||||
StopFunc func()
|
TriggerSelectionFunc func(haMap route.HAMap)
|
||||||
|
GetRouteSelectorFunc func() *routeselector.RouteSelector
|
||||||
|
StopFunc func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockManager) Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) {
|
func (m *MockManager) Init() (net.AddHookFunc, net.RemoveHookFunc, error) {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,11 +30,25 @@ 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) error {
|
func (m *MockManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, error) {
|
||||||
if m.UpdateRoutesFunc != nil {
|
if m.UpdateRoutesFunc != nil {
|
||||||
return m.UpdateRoutesFunc(updateSerial, newRoutes)
|
return m.UpdateRoutesFunc(updateSerial, newRoutes)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("method UpdateRoutes is not implemented")
|
return nil, nil, fmt.Errorf("method UpdateRoutes is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockManager) TriggerSelection(networks route.HAMap) {
|
||||||
|
if m.TriggerSelectionFunc != nil {
|
||||||
|
m.TriggerSelectionFunc(networks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRouteSelector mock implementation of GetRouteSelector from Manager interface
|
||||||
|
func (m *MockManager) GetRouteSelector() *routeselector.RouteSelector {
|
||||||
|
if m.GetRouteSelectorFunc != nil {
|
||||||
|
return m.GetRouteSelectorFunc()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start mock implementation of Start from Manager interface
|
// Start mock implementation of Start from Manager interface
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package routemanager
|
package routemanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -10,8 +11,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type notifier struct {
|
type notifier struct {
|
||||||
initialRouteRangers []string
|
initialRouteRanges []string
|
||||||
routeRangers []string
|
routeRanges []string
|
||||||
|
|
||||||
listener listener.NetworkChangeListener
|
listener listener.NetworkChangeListener
|
||||||
listenerMux sync.Mutex
|
listenerMux sync.Mutex
|
||||||
@@ -33,10 +34,10 @@ func (n *notifier) setInitialClientRoutes(clientRoutes []*route.Route) {
|
|||||||
nets = append(nets, r.Network.String())
|
nets = append(nets, r.Network.String())
|
||||||
}
|
}
|
||||||
sort.Strings(nets)
|
sort.Strings(nets)
|
||||||
n.initialRouteRangers = nets
|
n.initialRouteRanges = nets
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onNewRoutes(idMap map[string][]*route.Route) {
|
func (n *notifier) onNewRoutes(idMap route.HAMap) {
|
||||||
newNets := make([]string, 0)
|
newNets := make([]string, 0)
|
||||||
for _, routes := range idMap {
|
for _, routes := range idMap {
|
||||||
for _, r := range routes {
|
for _, r := range routes {
|
||||||
@@ -45,11 +46,18 @@ func (n *notifier) onNewRoutes(idMap map[string][]*route.Route) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(newNets)
|
sort.Strings(newNets)
|
||||||
if !n.hasDiff(n.initialRouteRangers, newNets) {
|
switch runtime.GOOS {
|
||||||
return
|
case "android":
|
||||||
|
if !n.hasDiff(n.initialRouteRanges, newNets) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if !n.hasDiff(n.routeRanges, newNets) {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
n.routeRangers = newNets
|
n.routeRanges = newNets
|
||||||
|
|
||||||
n.notify()
|
n.notify()
|
||||||
}
|
}
|
||||||
@@ -62,7 +70,7 @@ func (n *notifier) notify() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func(l listener.NetworkChangeListener) {
|
go func(l listener.NetworkChangeListener) {
|
||||||
l.OnNetworkChanged(strings.Join(n.routeRangers, ","))
|
l.OnNetworkChanged(strings.Join(addIPv6RangeIfNeeded(n.routeRanges), ","))
|
||||||
}(n.listener)
|
}(n.listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +86,20 @@ func (n *notifier) hasDiff(a []string, b []string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) initialRouteRanges() []string {
|
func (n *notifier) getInitialRouteRanges() []string {
|
||||||
return n.initialRouteRangers
|
return addIPv6RangeIfNeeded(n.initialRouteRanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addIPv6RangeIfNeeded returns the input ranges with the default IPv6 range when there is an IPv4 default route.
|
||||||
|
func addIPv6RangeIfNeeded(inputRanges []string) []string {
|
||||||
|
ranges := inputRanges
|
||||||
|
for _, r := range inputRanges {
|
||||||
|
// we are intentionally adding the ipv6 default range in case of ipv4 default range
|
||||||
|
// to ensure that all traffic is managed by the tunnel interface on android
|
||||||
|
if r == "0.0.0.0/0" {
|
||||||
|
ranges = append(ranges, "::/0")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ranges
|
||||||
}
|
}
|
||||||
|
|||||||
155
client/internal/routemanager/refcounter/refcounter.go
Normal file
155
client/internal/routemanager/refcounter/refcounter.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package refcounter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrIgnore can be returned by AddFunc to indicate that the counter not be incremented for the given prefix.
|
||||||
|
var ErrIgnore = errors.New("ignore")
|
||||||
|
|
||||||
|
type Ref[O any] struct {
|
||||||
|
Count int
|
||||||
|
Out O
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddFunc[I, O any] func(prefix netip.Prefix, in I) (out O, err error)
|
||||||
|
type RemoveFunc[I, O any] func(prefix netip.Prefix, out O) error
|
||||||
|
|
||||||
|
type Counter[I, O any] struct {
|
||||||
|
// refCountMap keeps track of the reference Ref for prefixes
|
||||||
|
refCountMap map[netip.Prefix]Ref[O]
|
||||||
|
refCountMu sync.Mutex
|
||||||
|
// idMap keeps track of the prefixes associated with an ID for removal
|
||||||
|
idMap map[string][]netip.Prefix
|
||||||
|
idMu sync.Mutex
|
||||||
|
add AddFunc[I, O]
|
||||||
|
remove RemoveFunc[I, O]
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Counter instance
|
||||||
|
func New[I, O any](add AddFunc[I, O], remove RemoveFunc[I, O]) *Counter[I, O] {
|
||||||
|
return &Counter[I, O]{
|
||||||
|
refCountMap: map[netip.Prefix]Ref[O]{},
|
||||||
|
idMap: map[string][]netip.Prefix{},
|
||||||
|
add: add,
|
||||||
|
remove: remove,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment increments the reference count for the given prefix.
|
||||||
|
// If this is the first reference to the prefix, the AddFunc is called.
|
||||||
|
func (rm *Counter[I, O]) Increment(prefix netip.Prefix, in I) (Ref[O], error) {
|
||||||
|
rm.refCountMu.Lock()
|
||||||
|
defer rm.refCountMu.Unlock()
|
||||||
|
|
||||||
|
ref := rm.refCountMap[prefix]
|
||||||
|
log.Tracef("Increasing ref count %d for prefix %s with [%v]", ref.Count, prefix, ref.Out)
|
||||||
|
|
||||||
|
// Call AddFunc only if it's a new prefix
|
||||||
|
if ref.Count == 0 {
|
||||||
|
log.Tracef("Adding for prefix %s with [%v]", prefix, ref.Out)
|
||||||
|
out, err := rm.add(prefix, in)
|
||||||
|
|
||||||
|
if errors.Is(err, ErrIgnore) {
|
||||||
|
return ref, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return ref, fmt.Errorf("failed to add for prefix %s: %w", prefix, err)
|
||||||
|
}
|
||||||
|
ref.Out = out
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.Count++
|
||||||
|
rm.refCountMap[prefix] = ref
|
||||||
|
|
||||||
|
return ref, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementWithID increments the reference count for the given prefix and groups it under the given ID.
|
||||||
|
// If this is the first reference to the prefix, the AddFunc is called.
|
||||||
|
func (rm *Counter[I, O]) IncrementWithID(id string, prefix netip.Prefix, in I) (Ref[O], error) {
|
||||||
|
rm.idMu.Lock()
|
||||||
|
defer rm.idMu.Unlock()
|
||||||
|
|
||||||
|
ref, err := rm.Increment(prefix, in)
|
||||||
|
if err != nil {
|
||||||
|
return ref, fmt.Errorf("with ID: %w", err)
|
||||||
|
}
|
||||||
|
rm.idMap[id] = append(rm.idMap[id], prefix)
|
||||||
|
|
||||||
|
return ref, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrement decrements the reference count for the given prefix.
|
||||||
|
// If the reference count reaches 0, the RemoveFunc is called.
|
||||||
|
func (rm *Counter[I, O]) Decrement(prefix netip.Prefix) (Ref[O], error) {
|
||||||
|
rm.refCountMu.Lock()
|
||||||
|
defer rm.refCountMu.Unlock()
|
||||||
|
|
||||||
|
ref, ok := rm.refCountMap[prefix]
|
||||||
|
if !ok {
|
||||||
|
log.Tracef("No reference found for prefix %s", prefix)
|
||||||
|
return ref, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Decreasing ref count %d for prefix %s with [%v]", ref.Count, prefix, ref.Out)
|
||||||
|
if ref.Count == 1 {
|
||||||
|
log.Tracef("Removing for prefix %s with [%v]", prefix, ref.Out)
|
||||||
|
if err := rm.remove(prefix, ref.Out); err != nil {
|
||||||
|
return ref, fmt.Errorf("remove for prefix %s: %w", prefix, err)
|
||||||
|
}
|
||||||
|
delete(rm.refCountMap, prefix)
|
||||||
|
} else {
|
||||||
|
ref.Count--
|
||||||
|
rm.refCountMap[prefix] = ref
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecrementWithID decrements the reference count for all prefixes associated with the given ID.
|
||||||
|
// If the reference count reaches 0, the RemoveFunc is called.
|
||||||
|
func (rm *Counter[I, O]) DecrementWithID(id string) error {
|
||||||
|
rm.idMu.Lock()
|
||||||
|
defer rm.idMu.Unlock()
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
for _, prefix := range rm.idMap[id] {
|
||||||
|
if _, err := rm.Decrement(prefix); err != nil {
|
||||||
|
merr = multierror.Append(merr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(rm.idMap, id)
|
||||||
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush removes all references and calls RemoveFunc for each prefix.
|
||||||
|
func (rm *Counter[I, O]) Flush() error {
|
||||||
|
rm.refCountMu.Lock()
|
||||||
|
defer rm.refCountMu.Unlock()
|
||||||
|
rm.idMu.Lock()
|
||||||
|
defer rm.idMu.Unlock()
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
for prefix := range rm.refCountMap {
|
||||||
|
log.Tracef("Removing for prefix %s", prefix)
|
||||||
|
ref := rm.refCountMap[prefix]
|
||||||
|
if err := rm.remove(prefix, ref.Out); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove for prefix %s: %w", prefix, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rm.refCountMap = map[netip.Prefix]Ref[O]{}
|
||||||
|
|
||||||
|
rm.idMap = map[string][]netip.Prefix{}
|
||||||
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
7
client/internal/routemanager/refcounter/types.go
Normal file
7
client/internal/routemanager/refcounter/types.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package refcounter
|
||||||
|
|
||||||
|
// RouteRefCounter is a Counter for Route, it doesn't take any input on Increment and doesn't use any output on Decrement
|
||||||
|
type RouteRefCounter = Counter[any, any]
|
||||||
|
|
||||||
|
// AllowedIPsRefCounter is a Counter for AllowedIPs, it takes a peer key on Increment and passes it back to Decrement
|
||||||
|
type AllowedIPsRefCounter = Counter[string, string]
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
//go:build !android && !ios
|
|
||||||
|
|
||||||
package routemanager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ref struct {
|
|
||||||
count int
|
|
||||||
nexthop netip.Addr
|
|
||||||
intf string
|
|
||||||
}
|
|
||||||
|
|
||||||
type RouteManager struct {
|
|
||||||
// refCountMap keeps track of the reference ref for prefixes
|
|
||||||
refCountMap map[netip.Prefix]ref
|
|
||||||
// prefixMap keeps track of the prefixes associated with a connection ID for removal
|
|
||||||
prefixMap map[nbnet.ConnectionID][]netip.Prefix
|
|
||||||
addRoute AddRouteFunc
|
|
||||||
removeRoute RemoveRouteFunc
|
|
||||||
mutex sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddRouteFunc func(prefix netip.Prefix) (nexthop netip.Addr, intf string, err error)
|
|
||||||
type RemoveRouteFunc func(prefix netip.Prefix, nexthop netip.Addr, intf string) error
|
|
||||||
|
|
||||||
func NewRouteManager(addRoute AddRouteFunc, removeRoute RemoveRouteFunc) *RouteManager {
|
|
||||||
// TODO: read initial routing table into refCountMap
|
|
||||||
return &RouteManager{
|
|
||||||
refCountMap: map[netip.Prefix]ref{},
|
|
||||||
prefixMap: map[nbnet.ConnectionID][]netip.Prefix{},
|
|
||||||
addRoute: addRoute,
|
|
||||||
removeRoute: removeRoute,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RouteManager) AddRouteRef(connID nbnet.ConnectionID, prefix netip.Prefix) error {
|
|
||||||
rm.mutex.Lock()
|
|
||||||
defer rm.mutex.Unlock()
|
|
||||||
|
|
||||||
ref := rm.refCountMap[prefix]
|
|
||||||
log.Debugf("Increasing route ref count %d for prefix %s", ref.count, prefix)
|
|
||||||
|
|
||||||
// Add route to the system, only if it's a new prefix
|
|
||||||
if ref.count == 0 {
|
|
||||||
log.Debugf("Adding route for prefix %s", prefix)
|
|
||||||
nexthop, intf, err := rm.addRoute(prefix)
|
|
||||||
if errors.Is(err, ErrRouteNotFound) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if errors.Is(err, ErrRouteNotAllowed) {
|
|
||||||
log.Debugf("Adding route for prefix %s: %s", prefix, err)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to add route for prefix %s: %w", prefix, err)
|
|
||||||
}
|
|
||||||
ref.nexthop = nexthop
|
|
||||||
ref.intf = intf
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.count++
|
|
||||||
rm.refCountMap[prefix] = ref
|
|
||||||
rm.prefixMap[connID] = append(rm.prefixMap[connID], prefix)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RouteManager) RemoveRouteRef(connID nbnet.ConnectionID) error {
|
|
||||||
rm.mutex.Lock()
|
|
||||||
defer rm.mutex.Unlock()
|
|
||||||
|
|
||||||
prefixes, ok := rm.prefixMap[connID]
|
|
||||||
if !ok {
|
|
||||||
log.Debugf("No prefixes found for connection ID %s", connID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var result *multierror.Error
|
|
||||||
for _, prefix := range prefixes {
|
|
||||||
ref := rm.refCountMap[prefix]
|
|
||||||
log.Debugf("Decreasing route ref count %d for prefix %s", ref.count, prefix)
|
|
||||||
if ref.count == 1 {
|
|
||||||
log.Debugf("Removing route for prefix %s", prefix)
|
|
||||||
// TODO: don't fail if the route is not found
|
|
||||||
if err := rm.removeRoute(prefix, ref.nexthop, ref.intf); err != nil {
|
|
||||||
result = multierror.Append(result, fmt.Errorf("remove route for prefix %s: %w", prefix, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
delete(rm.refCountMap, prefix)
|
|
||||||
} else {
|
|
||||||
ref.count--
|
|
||||||
rm.refCountMap[prefix] = ref
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete(rm.prefixMap, connID)
|
|
||||||
|
|
||||||
return result.ErrorOrNil()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush removes all references and routes from the system
|
|
||||||
func (rm *RouteManager) Flush() error {
|
|
||||||
rm.mutex.Lock()
|
|
||||||
defer rm.mutex.Unlock()
|
|
||||||
|
|
||||||
var result *multierror.Error
|
|
||||||
for prefix := range rm.refCountMap {
|
|
||||||
log.Debugf("Removing route for prefix %s", prefix)
|
|
||||||
ref := rm.refCountMap[prefix]
|
|
||||||
if err := rm.removeRoute(prefix, ref.nexthop, ref.intf); err != nil {
|
|
||||||
result = multierror.Append(result, fmt.Errorf("remove route for prefix %s: %w", prefix, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rm.refCountMap = map[netip.Prefix]ref{}
|
|
||||||
rm.prefixMap = map[nbnet.ConnectionID][]netip.Prefix{}
|
|
||||||
|
|
||||||
return result.ErrorOrNil()
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package routemanager
|
|||||||
import "github.com/netbirdio/netbird/route"
|
import "github.com/netbirdio/netbird/route"
|
||||||
|
|
||||||
type serverRouter interface {
|
type serverRouter interface {
|
||||||
updateRoutes(map[string]*route.Route) error
|
updateRoutes(map[route.ID]*route.Route) error
|
||||||
removeFromServerNetwork(*route.Route) error
|
removeFromServerNetwork(*route.Route) error
|
||||||
cleanUp()
|
cleanUp()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newServerRouter(context.Context, *iface.WGIface, firewall.Manager, *peer.Status) (serverRouter, error) {
|
func newServerRouter(context.Context, iface.IWGIface, firewall.Manager, *peer.Status) (serverRouter, error) {
|
||||||
return nil, fmt.Errorf("server route not supported on this os")
|
return nil, fmt.Errorf("server route not supported on this os")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"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/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
)
|
)
|
||||||
@@ -19,24 +20,24 @@ import (
|
|||||||
type defaultServerRouter struct {
|
type defaultServerRouter struct {
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
routes map[string]*route.Route
|
routes map[route.ID]*route.Route
|
||||||
firewall firewall.Manager
|
firewall firewall.Manager
|
||||||
wgInterface *iface.WGIface
|
wgInterface iface.IWGIface
|
||||||
statusRecorder *peer.Status
|
statusRecorder *peer.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServerRouter(ctx context.Context, wgInterface *iface.WGIface, firewall firewall.Manager, statusRecorder *peer.Status) (serverRouter, error) {
|
func newServerRouter(ctx context.Context, wgInterface iface.IWGIface, firewall firewall.Manager, statusRecorder *peer.Status) (serverRouter, error) {
|
||||||
return &defaultServerRouter{
|
return &defaultServerRouter{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
routes: make(map[string]*route.Route),
|
routes: make(map[route.ID]*route.Route),
|
||||||
firewall: firewall,
|
firewall: firewall,
|
||||||
wgInterface: wgInterface,
|
wgInterface: wgInterface,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *defaultServerRouter) updateRoutes(routesMap map[string]*route.Route) error {
|
func (m *defaultServerRouter) updateRoutes(routesMap map[route.ID]*route.Route) error {
|
||||||
serverRoutesToRemove := make([]string, 0)
|
serverRoutesToRemove := make([]route.ID, 0)
|
||||||
|
|
||||||
for routeID := range m.routes {
|
for routeID := range m.routes {
|
||||||
update, found := routesMap[routeID]
|
update, found := routesMap[routeID]
|
||||||
@@ -70,7 +71,7 @@ func (m *defaultServerRouter) updateRoutes(routesMap map[string]*route.Route) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(m.routes) > 0 {
|
if len(m.routes) > 0 {
|
||||||
err := enableIPForwarding()
|
err := systemops.EnableIPForwarding()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -88,7 +89,7 @@ func (m *defaultServerRouter) removeFromServerNetwork(route *route.Route) error
|
|||||||
m.mux.Lock()
|
m.mux.Lock()
|
||||||
defer m.mux.Unlock()
|
defer m.mux.Unlock()
|
||||||
|
|
||||||
routerPair, err := routeToRouterPair(m.wgInterface.Address().Masked().String(), route)
|
routerPair, err := routeToRouterPair(route)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse prefix: %w", err)
|
return fmt.Errorf("parse prefix: %w", err)
|
||||||
}
|
}
|
||||||
@@ -117,7 +118,7 @@ func (m *defaultServerRouter) addToServerNetwork(route *route.Route) error {
|
|||||||
m.mux.Lock()
|
m.mux.Lock()
|
||||||
defer m.mux.Unlock()
|
defer m.mux.Unlock()
|
||||||
|
|
||||||
routerPair, err := routeToRouterPair(m.wgInterface.Address().Masked().String(), route)
|
routerPair, err := routeToRouterPair(route)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse prefix: %w", err)
|
return fmt.Errorf("parse prefix: %w", err)
|
||||||
}
|
}
|
||||||
@@ -133,7 +134,13 @@ func (m *defaultServerRouter) addToServerNetwork(route *route.Route) error {
|
|||||||
if state.Routes == nil {
|
if state.Routes == nil {
|
||||||
state.Routes = map[string]struct{}{}
|
state.Routes = map[string]struct{}{}
|
||||||
}
|
}
|
||||||
state.Routes[route.Network.String()] = struct{}{}
|
|
||||||
|
routeStr := route.Network.String()
|
||||||
|
if route.IsDynamic() {
|
||||||
|
routeStr = route.Domains.SafeString()
|
||||||
|
}
|
||||||
|
state.Routes[routeStr] = struct{}{}
|
||||||
|
|
||||||
m.statusRecorder.UpdateLocalPeerState(state)
|
m.statusRecorder.UpdateLocalPeerState(state)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -144,7 +151,7 @@ func (m *defaultServerRouter) cleanUp() {
|
|||||||
m.mux.Lock()
|
m.mux.Lock()
|
||||||
defer m.mux.Unlock()
|
defer m.mux.Unlock()
|
||||||
for _, r := range m.routes {
|
for _, r := range m.routes {
|
||||||
routerPair, err := routeToRouterPair(m.wgInterface.Address().Masked().String(), r)
|
routerPair, err := routeToRouterPair(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to convert route to router pair: %v", err)
|
log.Errorf("Failed to convert route to router pair: %v", err)
|
||||||
continue
|
continue
|
||||||
@@ -162,15 +169,27 @@ func (m *defaultServerRouter) cleanUp() {
|
|||||||
m.statusRecorder.UpdateLocalPeerState(state)
|
m.statusRecorder.UpdateLocalPeerState(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
func routeToRouterPair(source string, route *route.Route) (firewall.RouterPair, error) {
|
func routeToRouterPair(route *route.Route) (firewall.RouterPair, error) {
|
||||||
parsed, err := netip.ParsePrefix(source)
|
// TODO: add ipv6
|
||||||
if err != nil {
|
source := getDefaultPrefix(route.Network)
|
||||||
return firewall.RouterPair{}, err
|
|
||||||
|
destination := route.Network.Masked().String()
|
||||||
|
if route.IsDynamic() {
|
||||||
|
// TODO: add ipv6
|
||||||
|
destination = "0.0.0.0/0"
|
||||||
}
|
}
|
||||||
|
|
||||||
return firewall.RouterPair{
|
return firewall.RouterPair{
|
||||||
ID: route.ID,
|
ID: string(route.ID),
|
||||||
Source: parsed.String(),
|
Source: source.String(),
|
||||||
Destination: route.Network.Masked().String(),
|
Destination: destination,
|
||||||
Masquerade: route.Masquerade,
|
Masquerade: route.Masquerade,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDefaultPrefix(prefix netip.Prefix) netip.Prefix {
|
||||||
|
if prefix.Addr().Is6() {
|
||||||
|
return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
|
||||||
|
}
|
||||||
|
return netip.PrefixFrom(netip.IPv4Unspecified(), 0)
|
||||||
|
}
|
||||||
|
|||||||
57
client/internal/routemanager/static/route.go
Normal file
57
client/internal/routemanager/static/route.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package static
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Route struct {
|
||||||
|
route *route.Route
|
||||||
|
routeRefCounter *refcounter.RouteRefCounter
|
||||||
|
allowedIPsRefcounter *refcounter.AllowedIPsRefCounter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRoute(rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allowedIPsRefCounter *refcounter.AllowedIPsRefCounter) *Route {
|
||||||
|
return &Route{
|
||||||
|
route: rt,
|
||||||
|
routeRefCounter: routeRefCounter,
|
||||||
|
allowedIPsRefcounter: allowedIPsRefCounter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route route methods
|
||||||
|
func (r *Route) String() string {
|
||||||
|
return r.route.Network.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) AddRoute(context.Context) error {
|
||||||
|
_, err := r.routeRefCounter.Increment(r.route.Network, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) RemoveRoute() error {
|
||||||
|
_, err := r.routeRefCounter.Decrement(r.route.Network)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) AddAllowedIPs(peerKey string) error {
|
||||||
|
if ref, err := r.allowedIPsRefcounter.Increment(r.route.Network, peerKey); err != nil {
|
||||||
|
return fmt.Errorf("add allowed IP %s: %w", r.route.Network, err)
|
||||||
|
} else if ref.Count > 1 && ref.Out != peerKey {
|
||||||
|
log.Warnf("Prefix [%s] is already routed by peer [%s]. HA routing disabled",
|
||||||
|
r.route.Network,
|
||||||
|
ref.Out,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) RemoveAllowedIPs() error {
|
||||||
|
_, err := r.allowedIPsRefcounter.Decrement(r.route.Network)
|
||||||
|
return err
|
||||||
|
}
|
||||||
103
client/internal/routemanager/sysctl/sysctl_linux.go
Normal file
103
client/internal/routemanager/sysctl/sysctl_linux.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
// go:build !android
|
||||||
|
package sysctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rpFilterPath = "net.ipv4.conf.all.rp_filter"
|
||||||
|
rpFilterInterfacePath = "net.ipv4.conf.%s.rp_filter"
|
||||||
|
srcValidMarkPath = "net.ipv4.conf.all.src_valid_mark"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup configures sysctl settings for RP filtering and source validation.
|
||||||
|
func Setup(wgIface iface.IWGIface) (map[string]int, error) {
|
||||||
|
keys := map[string]int{}
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
oldVal, err := Set(srcValidMarkPath, 1, false)
|
||||||
|
if err != nil {
|
||||||
|
result = multierror.Append(result, err)
|
||||||
|
} else {
|
||||||
|
keys[srcValidMarkPath] = oldVal
|
||||||
|
}
|
||||||
|
|
||||||
|
oldVal, err = Set(rpFilterPath, 2, true)
|
||||||
|
if err != nil {
|
||||||
|
result = multierror.Append(result, err)
|
||||||
|
} else {
|
||||||
|
keys[rpFilterPath] = oldVal
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
result = multierror.Append(result, fmt.Errorf("list interfaces: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, intf := range interfaces {
|
||||||
|
if intf.Name == "lo" || wgIface != nil && intf.Name == wgIface.Name() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
i := fmt.Sprintf(rpFilterInterfacePath, intf.Name)
|
||||||
|
oldVal, err := Set(i, 2, true)
|
||||||
|
if err != nil {
|
||||||
|
result = multierror.Append(result, err)
|
||||||
|
} else {
|
||||||
|
keys[i] = oldVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, nberrors.FormatErrorOrNil(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets a sysctl configuration, if onlyIfOne is true it will only set the new value if it's set to 1
|
||||||
|
func Set(key string, desiredValue int, onlyIfOne bool) (int, error) {
|
||||||
|
path := fmt.Sprintf("/proc/sys/%s", strings.ReplaceAll(key, ".", "/"))
|
||||||
|
currentValue, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return -1, fmt.Errorf("read sysctl %s: %w", key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentV, err := strconv.Atoi(strings.TrimSpace(string(currentValue)))
|
||||||
|
if err != nil && len(currentValue) > 0 {
|
||||||
|
return -1, fmt.Errorf("convert current desiredValue to int: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentV == desiredValue || onlyIfOne && currentV != 1 {
|
||||||
|
return currentV, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gosec
|
||||||
|
if err := os.WriteFile(path, []byte(strconv.Itoa(desiredValue)), 0644); err != nil {
|
||||||
|
return currentV, fmt.Errorf("write sysctl %s: %w", key, err)
|
||||||
|
}
|
||||||
|
log.Debugf("Set sysctl %s from %d to %d", key, currentV, desiredValue)
|
||||||
|
|
||||||
|
return currentV, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup resets sysctl settings to their original values.
|
||||||
|
func Cleanup(originalSettings map[string]int) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
for key, value := range originalSettings {
|
||||||
|
_, err := Set(key, value, false)
|
||||||
|
if err != nil {
|
||||||
|
result = multierror.Append(result, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nberrors.FormatErrorOrNil(result)
|
||||||
|
}
|
||||||
@@ -1,428 +0,0 @@
|
|||||||
//go:build !android && !ios
|
|
||||||
|
|
||||||
package routemanager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
|
||||||
"github.com/libp2p/go-netroute"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
|
||||||
"github.com/netbirdio/netbird/iface"
|
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
var splitDefaultv4_1 = netip.PrefixFrom(netip.IPv4Unspecified(), 1)
|
|
||||||
var splitDefaultv4_2 = netip.PrefixFrom(netip.AddrFrom4([4]byte{128}), 1)
|
|
||||||
var splitDefaultv6_1 = netip.PrefixFrom(netip.IPv6Unspecified(), 1)
|
|
||||||
var splitDefaultv6_2 = netip.PrefixFrom(netip.AddrFrom16([16]byte{0x80}), 1)
|
|
||||||
|
|
||||||
var ErrRouteNotFound = errors.New("route not found")
|
|
||||||
var ErrRouteNotAllowed = errors.New("route not allowed")
|
|
||||||
|
|
||||||
// TODO: fix: for default our wg address now appears as the default gw
|
|
||||||
func addRouteForCurrentDefaultGateway(prefix netip.Prefix) error {
|
|
||||||
addr := netip.IPv4Unspecified()
|
|
||||||
if prefix.Addr().Is6() {
|
|
||||||
addr = netip.IPv6Unspecified()
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultGateway, _, err := getNextHop(addr)
|
|
||||||
if err != nil && !errors.Is(err, ErrRouteNotFound) {
|
|
||||||
return fmt.Errorf("get existing route gateway: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !prefix.Contains(defaultGateway) {
|
|
||||||
log.Debugf("Skipping adding a new route for gateway %s because it is not in the network %s", defaultGateway, prefix)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gatewayPrefix := netip.PrefixFrom(defaultGateway, 32)
|
|
||||||
if defaultGateway.Is6() {
|
|
||||||
gatewayPrefix = netip.PrefixFrom(defaultGateway, 128)
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err := existsInRouteTable(gatewayPrefix)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to check if there is an existing route for gateway %s. error: %s", gatewayPrefix, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
log.Debugf("Skipping adding a new route for gateway %s because it already exists", gatewayPrefix)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var exitIntf string
|
|
||||||
gatewayHop, intf, err := getNextHop(defaultGateway)
|
|
||||||
if err != nil && !errors.Is(err, ErrRouteNotFound) {
|
|
||||||
return fmt.Errorf("unable to get the next hop for the default gateway address. error: %s", err)
|
|
||||||
}
|
|
||||||
if intf != nil {
|
|
||||||
exitIntf = intf.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Adding a new route for gateway %s with next hop %s", gatewayPrefix, gatewayHop)
|
|
||||||
return addToRouteTable(gatewayPrefix, gatewayHop, exitIntf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNextHop(ip netip.Addr) (netip.Addr, *net.Interface, error) {
|
|
||||||
r, err := netroute.New()
|
|
||||||
if err != nil {
|
|
||||||
return netip.Addr{}, nil, fmt.Errorf("new netroute: %w", err)
|
|
||||||
}
|
|
||||||
intf, gateway, preferredSrc, err := r.Route(ip.AsSlice())
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Failed to get route for %s: %v", ip, err)
|
|
||||||
return netip.Addr{}, nil, ErrRouteNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Route for %s: interface %v, nexthop %v, preferred source %v", ip, intf, gateway, preferredSrc)
|
|
||||||
if gateway == nil {
|
|
||||||
if preferredSrc == nil {
|
|
||||||
return netip.Addr{}, nil, ErrRouteNotFound
|
|
||||||
}
|
|
||||||
log.Debugf("No next hop found for ip %s, using preferred source %s", ip, preferredSrc)
|
|
||||||
|
|
||||||
addr, err := ipToAddr(preferredSrc, intf)
|
|
||||||
if err != nil {
|
|
||||||
return netip.Addr{}, nil, fmt.Errorf("convert preferred source to address: %w", err)
|
|
||||||
}
|
|
||||||
return addr.Unmap(), intf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
addr, err := ipToAddr(gateway, intf)
|
|
||||||
if err != nil {
|
|
||||||
return netip.Addr{}, nil, fmt.Errorf("convert gateway to address: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return addr, intf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// converts a net.IP to a netip.Addr including the zone based on the passed interface
|
|
||||||
func ipToAddr(ip net.IP, intf *net.Interface) (netip.Addr, error) {
|
|
||||||
addr, ok := netip.AddrFromSlice(ip)
|
|
||||||
if !ok {
|
|
||||||
return netip.Addr{}, fmt.Errorf("failed to convert IP address to netip.Addr: %s", ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
if intf != nil && (addr.IsLinkLocalMulticast() || addr.IsLinkLocalUnicast()) {
|
|
||||||
log.Tracef("Adding zone %s to address %s", intf.Name, addr)
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
addr = addr.WithZone(strconv.Itoa(intf.Index))
|
|
||||||
} else {
|
|
||||||
addr = addr.WithZone(intf.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return addr.Unmap(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func existsInRouteTable(prefix netip.Prefix) (bool, error) {
|
|
||||||
routes, err := getRoutesFromTable()
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("get routes from table: %w", err)
|
|
||||||
}
|
|
||||||
for _, tableRoute := range routes {
|
|
||||||
if tableRoute == prefix {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSubRange(prefix netip.Prefix) (bool, error) {
|
|
||||||
routes, err := getRoutesFromTable()
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("get routes from table: %w", err)
|
|
||||||
}
|
|
||||||
for _, tableRoute := range routes {
|
|
||||||
if tableRoute.Bits() > minRangeBits && tableRoute.Contains(prefix.Addr()) && tableRoute.Bits() < prefix.Bits() {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// addRouteToNonVPNIntf adds a new route to the routing table for the given prefix and returns the next hop and interface.
|
|
||||||
// If the next hop or interface is pointing to the VPN interface, it will return the initial values.
|
|
||||||
func addRouteToNonVPNIntf(
|
|
||||||
prefix netip.Prefix,
|
|
||||||
vpnIntf *iface.WGIface,
|
|
||||||
initialNextHop netip.Addr,
|
|
||||||
initialIntf *net.Interface,
|
|
||||||
) (netip.Addr, string, error) {
|
|
||||||
addr := prefix.Addr()
|
|
||||||
switch {
|
|
||||||
case addr.IsLoopback(),
|
|
||||||
addr.IsLinkLocalUnicast(),
|
|
||||||
addr.IsLinkLocalMulticast(),
|
|
||||||
addr.IsInterfaceLocalMulticast(),
|
|
||||||
addr.IsUnspecified(),
|
|
||||||
addr.IsMulticast():
|
|
||||||
|
|
||||||
return netip.Addr{}, "", ErrRouteNotAllowed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the exit interface and next hop for the prefix, so we can add a specific route
|
|
||||||
nexthop, intf, err := getNextHop(addr)
|
|
||||||
if err != nil {
|
|
||||||
return netip.Addr{}, "", fmt.Errorf("get next hop: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Found next hop %s for prefix %s with interface %v", nexthop, prefix, intf)
|
|
||||||
exitNextHop := nexthop
|
|
||||||
var exitIntf string
|
|
||||||
if intf != nil {
|
|
||||||
exitIntf = intf.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
vpnAddr, ok := netip.AddrFromSlice(vpnIntf.Address().IP)
|
|
||||||
if !ok {
|
|
||||||
return netip.Addr{}, "", fmt.Errorf("failed to convert vpn address to netip.Addr")
|
|
||||||
}
|
|
||||||
|
|
||||||
// if next hop is the VPN address or the interface is the VPN interface, we should use the initial values
|
|
||||||
if exitNextHop == vpnAddr || exitIntf == vpnIntf.Name() {
|
|
||||||
log.Debugf("Route for prefix %s is pointing to the VPN interface", prefix)
|
|
||||||
exitNextHop = initialNextHop
|
|
||||||
if initialIntf != nil {
|
|
||||||
exitIntf = initialIntf.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Adding a new route for prefix %s with next hop %s", prefix, exitNextHop)
|
|
||||||
if err := addToRouteTable(prefix, exitNextHop, exitIntf); err != nil {
|
|
||||||
return netip.Addr{}, "", fmt.Errorf("add route to table: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return exitNextHop, exitIntf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// genericAddVPNRoute adds a new route to the vpn interface, it splits the default prefix
|
|
||||||
// in two /1 prefixes to avoid replacing the existing default route
|
|
||||||
func genericAddVPNRoute(prefix netip.Prefix, intf string) error {
|
|
||||||
if prefix == defaultv4 {
|
|
||||||
if err := addToRouteTable(splitDefaultv4_1, netip.Addr{}, intf); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := addToRouteTable(splitDefaultv4_2, netip.Addr{}, intf); err != nil {
|
|
||||||
if err2 := removeFromRouteTable(splitDefaultv4_1, netip.Addr{}, intf); err2 != nil {
|
|
||||||
log.Warnf("Failed to rollback route addition: %s", err2)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: remove once IPv6 is supported on the interface
|
|
||||||
if err := addToRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err != nil {
|
|
||||||
return fmt.Errorf("add unreachable route split 1: %w", err)
|
|
||||||
}
|
|
||||||
if err := addToRouteTable(splitDefaultv6_2, netip.Addr{}, intf); err != nil {
|
|
||||||
if err2 := removeFromRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err2 != nil {
|
|
||||||
log.Warnf("Failed to rollback route addition: %s", err2)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("add unreachable route split 2: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
} else if prefix == defaultv6 {
|
|
||||||
if err := addToRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err != nil {
|
|
||||||
return fmt.Errorf("add unreachable route split 1: %w", err)
|
|
||||||
}
|
|
||||||
if err := addToRouteTable(splitDefaultv6_2, netip.Addr{}, intf); err != nil {
|
|
||||||
if err2 := removeFromRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err2 != nil {
|
|
||||||
log.Warnf("Failed to rollback route addition: %s", err2)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("add unreachable route split 2: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return addNonExistingRoute(prefix, intf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addNonExistingRoute adds a new route to the vpn interface if it doesn't exist in the current routing table
|
|
||||||
func addNonExistingRoute(prefix netip.Prefix, intf string) error {
|
|
||||||
ok, err := existsInRouteTable(prefix)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("exists in route table: %w", err)
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
log.Warnf("Skipping adding a new route for network %s because it already exists", prefix)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err = isSubRange(prefix)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("sub range: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
err := addRouteForCurrentDefaultGateway(prefix)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Unable to add route for current default gateway route. Will proceed without it. error: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return addToRouteTable(prefix, netip.Addr{}, intf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// genericRemoveVPNRoute removes the route from the vpn interface. If a default prefix is given,
|
|
||||||
// it will remove the split /1 prefixes
|
|
||||||
func genericRemoveVPNRoute(prefix netip.Prefix, intf string) error {
|
|
||||||
if prefix == defaultv4 {
|
|
||||||
var result *multierror.Error
|
|
||||||
if err := removeFromRouteTable(splitDefaultv4_1, netip.Addr{}, intf); err != nil {
|
|
||||||
result = multierror.Append(result, err)
|
|
||||||
}
|
|
||||||
if err := removeFromRouteTable(splitDefaultv4_2, netip.Addr{}, intf); err != nil {
|
|
||||||
result = multierror.Append(result, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: remove once IPv6 is supported on the interface
|
|
||||||
if err := removeFromRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err != nil {
|
|
||||||
result = multierror.Append(result, err)
|
|
||||||
}
|
|
||||||
if err := removeFromRouteTable(splitDefaultv6_2, netip.Addr{}, intf); err != nil {
|
|
||||||
result = multierror.Append(result, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ErrorOrNil()
|
|
||||||
} else if prefix == defaultv6 {
|
|
||||||
var result *multierror.Error
|
|
||||||
if err := removeFromRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err != nil {
|
|
||||||
result = multierror.Append(result, err)
|
|
||||||
}
|
|
||||||
if err := removeFromRouteTable(splitDefaultv6_2, netip.Addr{}, intf); err != nil {
|
|
||||||
result = multierror.Append(result, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ErrorOrNil()
|
|
||||||
}
|
|
||||||
|
|
||||||
return removeFromRouteTable(prefix, netip.Addr{}, intf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPrefixFromIP(ip net.IP) (*netip.Prefix, error) {
|
|
||||||
addr, ok := netip.AddrFromSlice(ip)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("parse IP address: %s", ip)
|
|
||||||
}
|
|
||||||
addr = addr.Unmap()
|
|
||||||
|
|
||||||
var prefixLength int
|
|
||||||
switch {
|
|
||||||
case addr.Is4():
|
|
||||||
prefixLength = 32
|
|
||||||
case addr.Is6():
|
|
||||||
prefixLength = 128
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid IP address: %s", addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := netip.PrefixFrom(addr, prefixLength)
|
|
||||||
return &prefix, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupRoutingWithRouteManager(routeManager **RouteManager, initAddresses []net.IP, wgIface *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) {
|
|
||||||
initialNextHopV4, initialIntfV4, err := getNextHop(netip.IPv4Unspecified())
|
|
||||||
if err != nil && !errors.Is(err, ErrRouteNotFound) {
|
|
||||||
log.Errorf("Unable to get initial v4 default next hop: %v", err)
|
|
||||||
}
|
|
||||||
initialNextHopV6, initialIntfV6, err := getNextHop(netip.IPv6Unspecified())
|
|
||||||
if err != nil && !errors.Is(err, ErrRouteNotFound) {
|
|
||||||
log.Errorf("Unable to get initial v6 default next hop: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*routeManager = NewRouteManager(
|
|
||||||
func(prefix netip.Prefix) (netip.Addr, string, error) {
|
|
||||||
addr := prefix.Addr()
|
|
||||||
nexthop, intf := initialNextHopV4, initialIntfV4
|
|
||||||
if addr.Is6() {
|
|
||||||
nexthop, intf = initialNextHopV6, initialIntfV6
|
|
||||||
}
|
|
||||||
return addRouteToNonVPNIntf(prefix, wgIface, nexthop, intf)
|
|
||||||
},
|
|
||||||
removeFromRouteTable,
|
|
||||||
)
|
|
||||||
|
|
||||||
return setupHooks(*routeManager, initAddresses)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanupRoutingWithRouteManager(routeManager *RouteManager) error {
|
|
||||||
if routeManager == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Remove hooks selectively
|
|
||||||
nbnet.RemoveDialerHooks()
|
|
||||||
nbnet.RemoveListenerHooks()
|
|
||||||
|
|
||||||
if err := routeManager.Flush(); err != nil {
|
|
||||||
return fmt.Errorf("flush route manager: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupHooks(routeManager *RouteManager, initAddresses []net.IP) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) {
|
|
||||||
beforeHook := func(connID nbnet.ConnectionID, ip net.IP) error {
|
|
||||||
prefix, err := getPrefixFromIP(ip)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("convert ip to prefix: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := routeManager.AddRouteRef(connID, *prefix); err != nil {
|
|
||||||
return fmt.Errorf("adding route reference: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
afterHook := func(connID nbnet.ConnectionID) error {
|
|
||||||
if err := routeManager.RemoveRouteRef(connID); err != nil {
|
|
||||||
return fmt.Errorf("remove route reference: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ip := range initAddresses {
|
|
||||||
if err := beforeHook("init", ip); err != nil {
|
|
||||||
log.Errorf("Failed to add route reference: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nbnet.AddDialerHook(func(ctx context.Context, connID nbnet.ConnectionID, resolvedIPs []net.IPAddr) error {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
var result *multierror.Error
|
|
||||||
for _, ip := range resolvedIPs {
|
|
||||||
result = multierror.Append(result, beforeHook(connID, ip.IP))
|
|
||||||
}
|
|
||||||
return result.ErrorOrNil()
|
|
||||||
})
|
|
||||||
|
|
||||||
nbnet.AddDialerCloseHook(func(connID nbnet.ConnectionID, conn *net.Conn) error {
|
|
||||||
return afterHook(connID)
|
|
||||||
})
|
|
||||||
|
|
||||||
nbnet.AddListenerWriteHook(func(connID nbnet.ConnectionID, ip *net.IPAddr, data []byte) error {
|
|
||||||
return beforeHook(connID, ip.IP)
|
|
||||||
})
|
|
||||||
|
|
||||||
nbnet.AddListenerCloseHook(func(connID nbnet.ConnectionID, conn net.PacketConn) error {
|
|
||||||
return afterHook(connID)
|
|
||||||
})
|
|
||||||
|
|
||||||
return beforeHook, afterHook, nil
|
|
||||||
}
|
|
||||||
18
client/internal/routemanager/systemops/routeflags_bsd.go
Normal file
18
client/internal/routemanager/systemops/routeflags_bsd.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build darwin || dragonfly || netbsd || openbsd
|
||||||
|
|
||||||
|
package systemops
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
// filterRoutesByFlags - return true if need to ignore such route message because it consists specific flags.
|
||||||
|
func filterRoutesByFlags(routeMessageFlags int) bool {
|
||||||
|
if routeMessageFlags&syscall.RTF_UP == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if routeMessageFlags&(syscall.RTF_REJECT|syscall.RTF_BLACKHOLE|syscall.RTF_WASCLONED) != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
19
client/internal/routemanager/systemops/routeflags_freebsd.go
Normal file
19
client/internal/routemanager/systemops/routeflags_freebsd.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//go:build: freebsd
|
||||||
|
package systemops
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
// filterRoutesByFlags - return true if need to ignore such route message because it consists specific flags.
|
||||||
|
func filterRoutesByFlags(routeMessageFlags int) bool {
|
||||||
|
if routeMessageFlags&syscall.RTF_UP == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: syscall.RTF_WASCLONED deprecated in FreeBSD 8.0 (https://www.freebsd.org/releases/8.0R/relnotes-detailed/)
|
||||||
|
// a concept of cloned route (a route generated by an entry with RTF_CLONING flag) is deprecated.
|
||||||
|
if routeMessageFlags&(syscall.RTF_REJECT|syscall.RTF_BLACKHOLE) != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user