mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-27 04:36:37 +00:00
Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20a73e3e14 | ||
|
|
fcf7786a85 | ||
|
|
a78fd69f80 | ||
|
|
4bd5029e7b | ||
|
|
f604956246 | ||
|
|
53c532bbb4 | ||
|
|
8b0a1bbae0 | ||
|
|
e965d6c022 | ||
|
|
11f8249eed | ||
|
|
d63a9ce4a7 | ||
|
|
9cb66bdb5d | ||
|
|
c8ace8bbbe | ||
|
|
509d23c7cf | ||
|
|
1db4027bea | ||
|
|
d4dbc322be | ||
|
|
e19d5dca7f | ||
|
|
157137e4ad | ||
|
|
7d7e576775 | ||
|
|
f37b43a542 | ||
|
|
7e262572a4 | ||
|
|
a768a0aa8a | ||
|
|
ed7ac81027 | ||
|
|
1f845f466c | ||
|
|
270f0e4ce8 | ||
|
|
d0c6d88971 | ||
|
|
4321b71984 | ||
|
|
e8d82c1bd3 | ||
|
|
6aa7a2c5e1 | ||
|
|
2e0bf61e9a | ||
|
|
126af9dffc | ||
|
|
4cdf2df660 | ||
|
|
9a4c9aa286 | ||
|
|
5ed61700ff | ||
|
|
84117a9fb7 | ||
|
|
92b612eba4 | ||
|
|
aeeaa21eed | ||
|
|
d228cd0cb1 | ||
|
|
b41f36fccd | ||
|
|
d2cde4a040 | ||
|
|
84879a356b | ||
|
|
ed2214f9a9 | ||
|
|
4388dcc20b | ||
|
|
4f1f0df7d2 | ||
|
|
08ddf04c5f | ||
|
|
b5ee2174a8 | ||
|
|
7218a3d563 | ||
|
|
04e4407ea7 | ||
|
|
06055af361 | ||
|
|
abd1230a69 | ||
|
|
f7de12daf8 | ||
|
|
c49fb0c40c | ||
|
|
6e9a162877 | ||
|
|
b4e03f4616 | ||
|
|
369a7ef345 | ||
|
|
c88e6a7342 | ||
|
|
2cd9b11e7d | ||
|
|
93d20e370b | ||
|
|
878ca6db22 | ||
|
|
2033650908 | ||
|
|
34c1c7d901 | ||
|
|
051fd3a4d7 | ||
|
|
af69a48745 | ||
|
|
68ff97ba84 | ||
|
|
c5705803a5 | ||
|
|
7e1ae448e0 | ||
|
|
518a2561a2 | ||
|
|
c75ffd0f4b | ||
|
|
e4ad6174ca | ||
|
|
6de313070a | ||
|
|
cd7d1a80c9 | ||
|
|
be7d829858 | ||
|
|
ed1872560f | ||
|
|
de898899a4 | ||
|
|
b63ec71aed | ||
|
|
1012172f04 | ||
|
|
788bb00ef1 | ||
|
|
4e5ee70b3d | ||
|
|
f1c00ae543 | ||
|
|
553a13588b | ||
|
|
586c0f5c3d | ||
|
|
c13f0b9f07 | ||
|
|
dd4ff61b51 | ||
|
|
e3657610bc | ||
|
|
e8733a37af | ||
|
|
3def84b111 | ||
|
|
47add9a9c3 | ||
|
|
09312b3e6d | ||
|
|
762a26dcea | ||
|
|
000ea72aec | ||
|
|
4b34a6d6df | ||
|
|
c39cd2f7b0 | ||
|
|
6dc3e8ca90 | ||
|
|
245863cd51 | ||
|
|
14e322d3f7 | ||
|
|
1be8c16e34 | ||
|
|
851de3fd4e | ||
|
|
c13288781f | ||
|
|
e34e0ccd12 | ||
|
|
95dc9cc16c | ||
|
|
d1c2b3d703 | ||
|
|
966661fe91 | ||
|
|
67ddaade58 | ||
|
|
138cf35e00 | ||
|
|
2555a6c3e8 | ||
|
|
86a66c6202 | ||
|
|
275d364df6 | ||
|
|
a3c5fa1307 | ||
|
|
75a69ca26b | ||
|
|
ae8e3ad6fe | ||
|
|
ff729f6755 | ||
|
|
7e1b20da5d | ||
|
|
d4a3ee9d87 | ||
|
|
49e9113e0f | ||
|
|
3bdfa3cc8e | ||
|
|
8c953c5a2c | ||
|
|
e95f0f7acb | ||
|
|
fa7b413fe7 | ||
|
|
295f0c755a | ||
|
|
a98f6f840a | ||
|
|
faad5a1e98 | ||
|
|
e8caa562b0 | ||
|
|
1aafc15607 | ||
|
|
06860c4c10 | ||
|
|
f883a10535 | ||
|
|
8ec7f1cd96 | ||
|
|
aae84e40e2 | ||
|
|
5623735234 | ||
|
|
f9f2d7c7ef | ||
|
|
35c7cae267 | ||
|
|
503a116f7c | ||
|
|
a454a1aa28 | ||
|
|
a88ac40b05 | ||
|
|
bfff6110aa | ||
|
|
f810feafdf | ||
|
|
57536da245 | ||
|
|
c9b5328f19 | ||
|
|
dab146ed87 | ||
|
|
b96e616844 | ||
|
|
0cba0f81e0 | ||
|
|
ebd70a569c | ||
|
|
e7b43253b0 | ||
|
|
d005cd32b0 | ||
|
|
fa0399d975 | ||
|
|
e6e9f0322f | ||
|
|
60ac8c3268 |
13
.github/workflows/golang-test-darwin.yml
vendored
13
.github/workflows/golang-test-darwin.yml
vendored
@@ -1,16 +1,19 @@
|
|||||||
name: Test Code Darwin
|
name: Test Code Darwin
|
||||||
on: [push,pull_request]
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
go-version: [1.18.x]
|
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: 1.18.x
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
|||||||
66
.github/workflows/golang-test-linux.yml
vendored
66
.github/workflows/golang-test-linux.yml
vendored
@@ -1,16 +1,22 @@
|
|||||||
name: Test Code Linux
|
name: Test Code Linux
|
||||||
on: [push,pull_request]
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.18.x]
|
arch: ['386','amd64']
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: 1.18.x
|
||||||
|
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
@@ -31,4 +37,56 @@ jobs:
|
|||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
|
run: GOARCH=${{ matrix.arch }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
|
||||||
|
|
||||||
|
test_client_on_docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.x
|
||||||
|
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
|
||||||
|
|
||||||
|
- name: Install modules
|
||||||
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: Generate Iface Test bin
|
||||||
|
run: go test -c -o iface-testing.bin ./iface/...
|
||||||
|
|
||||||
|
- name: Generate RouteManager Test bin
|
||||||
|
run: go test -c -o routemanager-testing.bin ./client/internal/routemanager/...
|
||||||
|
|
||||||
|
- name: Generate Engine Test bin
|
||||||
|
run: go test -c -o engine-testing.bin ./client/internal/*.go
|
||||||
|
|
||||||
|
- name: Generate Peer Test bin
|
||||||
|
run: go test -c -o peer-testing.bin ./client/internal/peer/...
|
||||||
|
|
||||||
|
- run: chmod +x *testing.bin
|
||||||
|
|
||||||
|
- name: Run Iface tests in docker
|
||||||
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/iface --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/iface-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- name: Run Engine tests in docker
|
||||||
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
|
|
||||||
|
- name: Run Peer tests in docker
|
||||||
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/peer --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/peer-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
25
.github/workflows/golang-test-windows.yml
vendored
25
.github/workflows/golang-test-windows.yml
vendored
@@ -1,5 +1,11 @@
|
|||||||
name: Test Code Windows
|
name: Test Code Windows
|
||||||
on: [push,pull_request]
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre:
|
pre:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -17,41 +23,30 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
needs: pre
|
needs: pre
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
go-version: [1.18.x]
|
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: disable defender
|
|
||||||
run: Set-MpPreference -DisableRealtimeMonitoring $true
|
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: 1.18.x
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
%LocalAppData%\go-build
|
%LocalAppData%\go-build
|
||||||
~/go/pkg/mod
|
~\go\pkg\mod
|
||||||
|
~\AppData\Local\go-build
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: enable defender
|
|
||||||
run: Set-MpPreference -DisableRealtimeMonitoring $false
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v2
|
- uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: syso
|
name: syso
|
||||||
path: iface\
|
path: iface\
|
||||||
|
|
||||||
# - name: Install modules
|
|
||||||
# run: go mod tidy
|
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -tags=load_wgnt_from_rsrc -timeout 5m -p 1 ./...
|
run: go test -tags=load_wgnt_from_rsrc -timeout 5m -p 1 ./...
|
||||||
8
.github/workflows/golangci-lint.yml
vendored
8
.github/workflows/golangci-lint.yml
vendored
@@ -6,12 +6,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.x
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
args: --timeout=6m
|
# SA1019: "io/ioutil" has been deprecated since Go 1.16
|
||||||
|
args: --timeout=6m -e SA1019
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
122
.github/workflows/release.yml
vendored
122
.github/workflows/release.yml
vendored
@@ -9,7 +9,8 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.0.3"
|
SIGN_PIPE_VER: "v0.0.4"
|
||||||
|
GORELEASER_VER: "v1.6.3"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -40,6 +41,9 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Install modules
|
name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
-
|
||||||
|
name: check git status
|
||||||
|
run: git --no-pager diff --exit-code
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
@@ -54,45 +58,75 @@ jobs:
|
|||||||
username: netbirdio
|
username: netbirdio
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-mingw-w64-x86-64
|
|
||||||
|
|
||||||
- name: Install rsrc
|
|
||||||
run: go install github.com/akavel/rsrc@v0.10.2
|
|
||||||
|
|
||||||
- name: Generate windows rsrc
|
|
||||||
run: rsrc -arch amd64 -ico client/ui/netbird.ico -manifest client/ui/manifest.xml -o client/ui/resources_windows_amd64.syso
|
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Run GoReleaser
|
name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v2
|
uses: goreleaser/goreleaser-action@v2
|
||||||
with:
|
with:
|
||||||
version: v1.6.3
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --rm-dist
|
args: release --rm-dist
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||||
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
-
|
|
||||||
name: Trigger Windows binaries sign pipeline
|
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
workflow: Sign windows bin and installer
|
|
||||||
repo: netbirdio/sign-pipelines
|
|
||||||
ref: ${{ env.SIGN_PIPE_VER }}
|
|
||||||
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
|
||||||
inputs: '{ "tag": "${{ github.ref }}" }'
|
|
||||||
-
|
-
|
||||||
name: upload non tags for debug purposes
|
name: upload non tags for debug purposes
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: build
|
name: release
|
||||||
path: dist/
|
path: dist/
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
|
||||||
release_ui:
|
release_ui:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-ui-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-ui-go-
|
||||||
|
|
||||||
|
- name: Install modules
|
||||||
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: check git status
|
||||||
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-mingw-w64-x86-64
|
||||||
|
- name: Install rsrc
|
||||||
|
run: go install github.com/akavel/rsrc@v0.10.2
|
||||||
|
- name: Generate windows rsrc
|
||||||
|
run: rsrc -arch amd64 -ico client/ui/netbird.ico -manifest client/ui/manifest.xml -o client/ui/resources_windows_amd64.syso
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v2
|
||||||
|
with:
|
||||||
|
version: ${{ env.GORELEASER_VER }}
|
||||||
|
args: release --config .goreleaser_ui.yaml --rm-dist
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||||
|
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
|
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
|
- name: upload non tags for debug purposes
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: release-ui
|
||||||
|
path: dist/
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
|
release_ui_darwin:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
@@ -110,9 +144,9 @@ jobs:
|
|||||||
uses: actions/cache@v1
|
uses: actions/cache@v1
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-ui-go-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-
|
${{ runner.os }}-ui-go-
|
||||||
-
|
-
|
||||||
name: Install modules
|
name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
@@ -121,26 +155,42 @@ jobs:
|
|||||||
id: goreleaser
|
id: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@v2
|
uses: goreleaser/goreleaser-action@v2
|
||||||
with:
|
with:
|
||||||
version: v1.6.3
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --config .goreleaser_ui_darwin.yaml --rm-dist
|
args: release --config .goreleaser_ui_darwin.yaml --rm-dist
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Trigger Darwin App binaries sign pipeline
|
name: upload non tags for debug purposes
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: release-ui-darwin
|
||||||
|
path: dist/
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
|
trigger_windows_signer:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [release,release_ui]
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
steps:
|
||||||
|
- name: Trigger Windows binaries sign pipeline
|
||||||
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
|
with:
|
||||||
|
workflow: Sign windows bin and installer
|
||||||
|
repo: netbirdio/sign-pipelines
|
||||||
|
ref: ${{ env.SIGN_PIPE_VER }}
|
||||||
|
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
||||||
|
inputs: '{ "tag": "${{ github.ref }}" }'
|
||||||
|
|
||||||
|
trigger_darwin_signer:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release_ui_darwin
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
steps:
|
||||||
|
- name: Trigger Darwin App binaries sign pipeline
|
||||||
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
with:
|
with:
|
||||||
workflow: Sign darwin ui app with dispatch
|
workflow: Sign darwin ui app with dispatch
|
||||||
repo: netbirdio/sign-pipelines
|
repo: netbirdio/sign-pipelines
|
||||||
ref: ${{ env.SIGN_PIPE_VER }}
|
ref: ${{ env.SIGN_PIPE_VER }}
|
||||||
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
||||||
inputs: '{ "tag": "${{ github.ref }}" }'
|
inputs: '{ "tag": "${{ github.ref }}" }'
|
||||||
|
|
||||||
-
|
|
||||||
name: upload non tags for debug purposes
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: build-ui-darwin
|
|
||||||
path: dist/
|
|
||||||
retention-days: 3
|
|
||||||
|
|||||||
80
.github/workflows/test-docker-compose-linux.yml
vendored
Normal file
80
.github/workflows/test-docker-compose-linux.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
name: Test Docker Compose Linux
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install jq
|
||||||
|
run: sudo apt-get install -y jq
|
||||||
|
|
||||||
|
- name: Install curl
|
||||||
|
run: sudo apt-get install -y curl
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.x
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: cp setup.env
|
||||||
|
run: cp infrastructure_files/tests/setup.env infrastructure_files/
|
||||||
|
|
||||||
|
- name: run configure
|
||||||
|
working-directory: infrastructure_files
|
||||||
|
run: bash -x configure.sh
|
||||||
|
env:
|
||||||
|
CI_NETBIRD_AUTH_CLIENT_ID: ${{ secrets.CI_NETBIRD_AUTH_CLIENT_ID }}
|
||||||
|
CI_NETBIRD_AUTH_AUDIENCE: testing.ci
|
||||||
|
CI_NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT: https://example.eu.auth0.com/.well-known/openid-configuration
|
||||||
|
CI_NETBIRD_USE_AUTH0: true
|
||||||
|
|
||||||
|
- name: check values
|
||||||
|
working-directory: infrastructure_files
|
||||||
|
env:
|
||||||
|
CI_NETBIRD_AUTH_CLIENT_ID: ${{ secrets.CI_NETBIRD_AUTH_CLIENT_ID }}
|
||||||
|
CI_NETBIRD_AUTH_AUDIENCE: testing.ci
|
||||||
|
CI_NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT: https://example.eu.auth0.com/.well-known/openid-configuration
|
||||||
|
CI_NETBIRD_USE_AUTH0: true
|
||||||
|
CI_NETBIRD_AUTH_SUPPORTED_SCOPES: "openid profile email offline_access api email_verified"
|
||||||
|
CI_NETBIRD_AUTH_AUTHORITY: https://example.eu.auth0.com/
|
||||||
|
CI_NETBIRD_AUTH_JWT_CERTS: https://example.eu.auth0.com/.well-known/jwks.json
|
||||||
|
CI_NETBIRD_AUTH_TOKEN_ENDPOINT: https://example.eu.auth0.com/oauth/token
|
||||||
|
CI_NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT: https://example.eu.auth0.com/oauth/device/code
|
||||||
|
CI_NETBIRD_AUTH_REDIRECT_URI: "/peers"
|
||||||
|
run: |
|
||||||
|
grep AUTH_CLIENT_ID docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_ID
|
||||||
|
grep AUTH_AUTHORITY docker-compose.yml | grep $CI_NETBIRD_AUTH_AUTHORITY
|
||||||
|
grep AUTH_AUDIENCE docker-compose.yml | grep $CI_NETBIRD_AUTH_AUDIENCE
|
||||||
|
grep AUTH_SUPPORTED_SCOPES docker-compose.yml | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
|
||||||
|
grep USE_AUTH0 docker-compose.yml | grep $CI_NETBIRD_USE_AUTH0
|
||||||
|
grep NETBIRD_MGMT_API_ENDPOINT docker-compose.yml | grep "http://localhost:33073"
|
||||||
|
grep AUTH_REDIRECT_URI docker-compose.yml | grep $CI_NETBIRD_AUTH_REDIRECT_URI
|
||||||
|
grep AUTH_SILENT_REDIRECT_URI docker-compose.yml | egrep 'AUTH_SILENT_REDIRECT_URI=$'
|
||||||
|
|
||||||
|
- name: run docker compose up
|
||||||
|
working-directory: infrastructure_files
|
||||||
|
run: |
|
||||||
|
docker-compose up -d
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
- name: test running containers
|
||||||
|
run: |
|
||||||
|
count=$(docker compose ps --format json | jq '.[] | select(.Project | contains("infrastructure_files")) | .State' | grep -c running)
|
||||||
|
test $count -eq 4
|
||||||
|
working-directory: infrastructure_files
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.idea
|
.idea
|
||||||
|
.run
|
||||||
*.iml
|
*.iml
|
||||||
dist/
|
dist/
|
||||||
bin/
|
bin/
|
||||||
@@ -9,3 +10,5 @@ infrastructure_files/management.json
|
|||||||
infrastructure_files/docker-compose.yml
|
infrastructure_files/docker-compose.yml
|
||||||
*.syso
|
*.syso
|
||||||
client/.distfiles/
|
client/.distfiles/
|
||||||
|
infrastructure_files/setup.env
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- mips
|
- mips
|
||||||
|
- 386
|
||||||
gomips:
|
gomips:
|
||||||
- hardfloat
|
- hardfloat
|
||||||
- softfloat
|
- softfloat
|
||||||
@@ -21,6 +22,8 @@ builds:
|
|||||||
goarch: arm64
|
goarch: arm64
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: arm
|
goarch: arm
|
||||||
|
- goos: windows
|
||||||
|
goarch: 386
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
@@ -38,7 +41,7 @@ builds:
|
|||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
|
||||||
- id: netbird-signal
|
- id: netbird-signal
|
||||||
@@ -55,88 +58,12 @@ builds:
|
|||||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
|
||||||
- id: netbird-ui
|
|
||||||
dir: client/ui
|
|
||||||
binary: netbird-ui
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=1
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
goarch:
|
|
||||||
- amd64
|
|
||||||
ldflags:
|
|
||||||
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
|
||||||
|
|
||||||
- id: netbird-ui-windows
|
|
||||||
dir: client/ui
|
|
||||||
binary: netbird-ui
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=1
|
|
||||||
- CC=x86_64-w64-mingw32-gcc
|
|
||||||
goos:
|
|
||||||
- windows
|
|
||||||
goarch:
|
|
||||||
- amd64
|
|
||||||
ldflags:
|
|
||||||
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
|
||||||
- -H windowsgui
|
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- builds:
|
- builds:
|
||||||
- netbird
|
- netbird
|
||||||
- id: linux-arch
|
|
||||||
name_template: "{{ .ProjectName }}-ui-linux_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
|
||||||
builds:
|
|
||||||
- netbird-ui
|
|
||||||
- id: windows-arch
|
|
||||||
name_template: "{{ .ProjectName }}-ui-windows_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
|
||||||
builds:
|
|
||||||
- netbird-ui-windows
|
|
||||||
|
|
||||||
nfpms:
|
nfpms:
|
||||||
|
|
||||||
- maintainer: Netbird <dev@netbird.io>
|
|
||||||
description: Netbird client UI.
|
|
||||||
homepage: https://netbird.io/
|
|
||||||
id: netbird-ui-deb
|
|
||||||
package_name: netbird-ui
|
|
||||||
builds:
|
|
||||||
- netbird-ui
|
|
||||||
formats:
|
|
||||||
- deb
|
|
||||||
contents:
|
|
||||||
- src: client/ui/netbird.desktop
|
|
||||||
dst: /usr/share/applications/netbird.desktop
|
|
||||||
- src: client/ui/disconnected.png
|
|
||||||
dst: /usr/share/pixmaps/netbird.png
|
|
||||||
dependencies:
|
|
||||||
- libayatana-appindicator3-1
|
|
||||||
- libgtk-3-dev
|
|
||||||
- libappindicator3-dev
|
|
||||||
- netbird
|
|
||||||
|
|
||||||
- maintainer: Netbird <dev@netbird.io>
|
|
||||||
description: Netbird client UI.
|
|
||||||
homepage: https://netbird.io/
|
|
||||||
id: netbird-ui-rpm
|
|
||||||
package_name: netbird-ui
|
|
||||||
builds:
|
|
||||||
- netbird-ui
|
|
||||||
formats:
|
|
||||||
- rpm
|
|
||||||
contents:
|
|
||||||
- src: client/ui/netbird.desktop
|
|
||||||
dst: /usr/share/applications/netbird.desktop
|
|
||||||
- src: client/ui/disconnected.png
|
|
||||||
dst: /usr/share/pixmaps/netbird.png
|
|
||||||
dependencies:
|
|
||||||
- libayatana-appindicator3-1
|
|
||||||
- libgtk-3-dev
|
|
||||||
- libappindicator3-dev
|
|
||||||
- netbird
|
|
||||||
|
|
||||||
- maintainer: Netbird <dev@netbird.io>
|
- maintainer: Netbird <dev@netbird.io>
|
||||||
description: Netbird client.
|
description: Netbird client.
|
||||||
homepage: https://netbird.io/
|
homepage: https://netbird.io/
|
||||||
@@ -428,7 +355,6 @@ uploads:
|
|||||||
- name: debian
|
- name: debian
|
||||||
ids:
|
ids:
|
||||||
- netbird-deb
|
- netbird-deb
|
||||||
- netbird-ui-deb
|
|
||||||
mode: archive
|
mode: archive
|
||||||
target: https://pkgs.wiretrustee.com/debian/pool/{{ .ArtifactName }};deb.distribution=stable;deb.component=main;deb.architecture={{ if .Arm }}armhf{{ else }}{{ .Arch }}{{ end }};deb.package=
|
target: https://pkgs.wiretrustee.com/debian/pool/{{ .ArtifactName }};deb.distribution=stable;deb.component=main;deb.architecture={{ if .Arm }}armhf{{ else }}{{ .Arch }}{{ end }};deb.package=
|
||||||
username: dev@wiretrustee.com
|
username: dev@wiretrustee.com
|
||||||
@@ -437,7 +363,6 @@ uploads:
|
|||||||
- name: yum
|
- name: yum
|
||||||
ids:
|
ids:
|
||||||
- netbird-rpm
|
- netbird-rpm
|
||||||
- netbird-ui-rpm
|
|
||||||
mode: archive
|
mode: archive
|
||||||
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
||||||
username: dev@wiretrustee.com
|
username: dev@wiretrustee.com
|
||||||
|
|||||||
98
.goreleaser_ui.yaml
Normal file
98
.goreleaser_ui.yaml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
project_name: netbird-ui
|
||||||
|
builds:
|
||||||
|
- id: netbird-ui
|
||||||
|
dir: client/ui
|
||||||
|
binary: netbird-ui
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=1
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
|
||||||
|
- id: netbird-ui-windows
|
||||||
|
dir: client/ui
|
||||||
|
binary: netbird-ui
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=1
|
||||||
|
- CC=x86_64-w64-mingw32-gcc
|
||||||
|
goos:
|
||||||
|
- windows
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
|
- -H windowsgui
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- id: linux-arch
|
||||||
|
name_template: "{{ .ProjectName }}-linux_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||||
|
builds:
|
||||||
|
- netbird-ui
|
||||||
|
- id: windows-arch
|
||||||
|
name_template: "{{ .ProjectName }}-windows_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||||
|
builds:
|
||||||
|
- netbird-ui-windows
|
||||||
|
|
||||||
|
nfpms:
|
||||||
|
|
||||||
|
- maintainer: Netbird <dev@netbird.io>
|
||||||
|
description: Netbird client UI.
|
||||||
|
homepage: https://netbird.io/
|
||||||
|
id: netbird-ui-deb
|
||||||
|
package_name: netbird-ui
|
||||||
|
builds:
|
||||||
|
- netbird-ui
|
||||||
|
formats:
|
||||||
|
- deb
|
||||||
|
contents:
|
||||||
|
- src: client/ui/netbird.desktop
|
||||||
|
dst: /usr/share/applications/netbird.desktop
|
||||||
|
- src: client/ui/disconnected.png
|
||||||
|
dst: /usr/share/pixmaps/netbird.png
|
||||||
|
dependencies:
|
||||||
|
- libayatana-appindicator3-1
|
||||||
|
- libgtk-3-dev
|
||||||
|
- libappindicator3-dev
|
||||||
|
- netbird
|
||||||
|
|
||||||
|
- maintainer: Netbird <dev@netbird.io>
|
||||||
|
description: Netbird client UI.
|
||||||
|
homepage: https://netbird.io/
|
||||||
|
id: netbird-ui-rpm
|
||||||
|
package_name: netbird-ui
|
||||||
|
builds:
|
||||||
|
- netbird-ui
|
||||||
|
formats:
|
||||||
|
- rpm
|
||||||
|
contents:
|
||||||
|
- src: client/ui/netbird.desktop
|
||||||
|
dst: /usr/share/applications/netbird.desktop
|
||||||
|
- src: client/ui/disconnected.png
|
||||||
|
dst: /usr/share/pixmaps/netbird.png
|
||||||
|
dependencies:
|
||||||
|
- libayatana-appindicator3-1
|
||||||
|
- libgtk-3-dev
|
||||||
|
- libappindicator3-dev
|
||||||
|
- netbird
|
||||||
|
|
||||||
|
uploads:
|
||||||
|
- name: debian
|
||||||
|
ids:
|
||||||
|
- netbird-ui-deb
|
||||||
|
mode: archive
|
||||||
|
target: https://pkgs.wiretrustee.com/debian/pool/{{ .ArtifactName }};deb.distribution=stable;deb.component=main;deb.architecture={{ if .Arm }}armhf{{ else }}{{ .Arch }}{{ end }};deb.package=
|
||||||
|
username: dev@wiretrustee.com
|
||||||
|
method: PUT
|
||||||
|
|
||||||
|
- name: yum
|
||||||
|
ids:
|
||||||
|
- netbird-ui-rpm
|
||||||
|
mode: archive
|
||||||
|
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
||||||
|
username: dev@wiretrustee.com
|
||||||
|
method: PUT
|
||||||
@@ -14,7 +14,7 @@ builds:
|
|||||||
- hardfloat
|
- hardfloat
|
||||||
- softfloat
|
- softfloat
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/netbirdio/netbird/client/ui/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
tags:
|
tags:
|
||||||
- load_wgnt_from_rsrc
|
- load_wgnt_from_rsrc
|
||||||
@@ -23,5 +23,7 @@ archives:
|
|||||||
- builds:
|
- builds:
|
||||||
- netbird-ui-darwin
|
- netbird-ui-darwin
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: "{{ .ProjectName }}_darwin_checksums.txt"
|
||||||
changelog:
|
changelog:
|
||||||
skip: true
|
skip: true
|
||||||
98
README.md
98
README.md
@@ -1,32 +1,22 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>:hatching_chick: New release! Beta Update May 2022</strong>.
|
<strong>:hatching_chick: New Release! DNS support.</strong>
|
||||||
<a href="https://github.com/netbirdio/netbird/releases/tag/v0.6.0">
|
<a href="https://github.com/netbirdio/netbird/releases">
|
||||||
Learn more
|
Learn more
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img width="234" src="docs/media/logo-full.png"/>
|
<img width="234" src="docs/media/logo-full.png"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/netbirdio/netbird/blob/main/LICENSE">
|
<a href="https://github.com/netbirdio/netbird/blob/main/LICENSE">
|
||||||
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
|
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://hub.docker.com/r/wiretrustee/wiretrustee/tags">
|
<a href="https://www.codacy.com/gh/netbirdio/netbird/dashboard?utm_source=github.com&utm_medium=referral&utm_content=netbirdio/netbird&utm_campaign=Badge_Grade"><img src="https://app.codacy.com/project/badge/Grade/e3013d046aec44cdb7462c8673b00976"/></a>
|
||||||
<img src="https://img.shields.io/docker/pulls/wiretrustee/wiretrustee" />
|
|
||||||
</a>
|
|
||||||
<br>
|
<br>
|
||||||
<a href="https://www.codacy.com/gh/wiretrustee/wiretrustee/dashboard?utm_source=github.com&utm_medium=referral&utm_content=wiretrustee/wiretrustee&utm_campaign=Badge_Grade"><img src="https://app.codacy.com/project/badge/Grade/d366de2c9d8b4cf982da27f8f5831809"/></a>
|
<a href="https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">
|
||||||
<a href="https://goreportcard.com/report/wiretrustee/wiretrustee">
|
<img src="https://img.shields.io/badge/slack-@netbird-red.svg?logo=slack"/>
|
||||||
<img src="https://goreportcard.com/badge/github.com/wiretrustee/wiretrustee?style=flat-square" />
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<a href="https://join.slack.com/t/wiretrustee/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">
|
|
||||||
<img src="https://img.shields.io/badge/slack-@wiretrustee-red.svg?logo=slack"/>
|
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,7 +28,7 @@
|
|||||||
<br/>
|
<br/>
|
||||||
See <a href="https://netbird.io/docs/">Documentation</a>
|
See <a href="https://netbird.io/docs/">Documentation</a>
|
||||||
<br/>
|
<br/>
|
||||||
Join our <a href="https://join.slack.com/t/wiretrustee/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">Slack channel</a>
|
Join our <a href="https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">Slack channel</a>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
</strong>
|
</strong>
|
||||||
@@ -50,51 +40,50 @@
|
|||||||
|
|
||||||
It requires zero configuration effort leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
It requires zero configuration effort leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
||||||
|
|
||||||
NetBird creates an overlay peer-to-peer network connecting machines automatically regardless of their location (home, office, datacenter, container, cloud or edge environments) unifying virtual private network management experience.
|
NetBird uses [NAT traversal techniques](https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment) to automatically create an overlay peer-to-peer network connecting machines regardless of location (home, office, data center, container, cloud, or edge environments), unifying virtual private network management experience.
|
||||||
|
|
||||||
**Key features:**
|
**Key features:**
|
||||||
* Automatic IP allocation and management.
|
- \[x] Automatic IP allocation and network management with a Web UI ([separate repo](https://github.com/netbirdio/dashboard))
|
||||||
* Automatic WireGuard peer (machine) discovery and configuration.
|
- \[x] Automatic WireGuard peer (machine) discovery and configuration.
|
||||||
* Encrypted peer-to-peer connections without a central VPN gateway.
|
- \[x] Encrypted peer-to-peer connections without a central VPN gateway.
|
||||||
* Connection relay fallback in case a peer-to-peer connection is not possible.
|
- \[x] Connection relay fallback in case a peer-to-peer connection is not possible.
|
||||||
* Network management layer with a neat Web UI panel ([separate repo](https://github.com/netbirdio/dashboard))
|
- \[x] Desktop client applications for Linux, MacOS, and Windows (systray).
|
||||||
* Desktop client applications for Linux, MacOS, and Windows.
|
- \[x] Multiuser support - sharing network between multiple users.
|
||||||
* Multiuser support - sharing network between multiple users.
|
- \[x] SSO and MFA support.
|
||||||
* SSO and MFA support.
|
- \[x] Multicloud and hybrid-cloud support.
|
||||||
* Multicloud and hybrid-cloud support.
|
- \[x] Kernel WireGuard usage when possible.
|
||||||
* Kernel WireGuard usage when possible.
|
- \[x] Access Controls - groups & rules.
|
||||||
* Access Controls - groups & rules (coming soon).
|
- \[x] Remote SSH access without managing SSH keys.
|
||||||
* Private DNS (coming soon).
|
- \[x] Network Routes.
|
||||||
* Mobile clients (coming soon).
|
- \[x] Private DNS.
|
||||||
* Network Activity Monitoring (coming soon).
|
|
||||||
|
**Coming soon:**
|
||||||
|
- \[ ] Mobile clients.
|
||||||
|
- \[ ] Network Activity Monitoring.
|
||||||
|
|
||||||
### Secure peer-to-peer VPN with SSO and MFA in minutes
|
### Secure peer-to-peer VPN with SSO and MFA in minutes
|
||||||
<p float="left" align="middle">
|
|
||||||
<img src="docs/media/peerA.gif" width="400"/>
|
https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov
|
||||||
<img src="docs/media/peerB.gif" width="400"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
||||||
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
|
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
|
||||||
|
|
||||||
### Start using NetBird
|
### Start using NetBird
|
||||||
* Hosted version: [https://app.netbird.io/](https://app.netbird.io/).
|
- Hosted version: [https://app.netbird.io/](https://app.netbird.io/).
|
||||||
* See our documentation for [Quickstart Guide](https://netbird.io/docs/getting-started/quickstart).
|
- See our documentation for [Quickstart Guide](https://netbird.io/docs/getting-started/quickstart).
|
||||||
* If you are looking to self-host NetBird, check our [Self-Hosting Guide](https://netbird.io/docs/getting-started/self-hosting).
|
- If you are looking to self-host NetBird, check our [Self-Hosting Guide](https://netbird.io/docs/getting-started/self-hosting).
|
||||||
* Step-by-step [Installation Guide](https://netbird.io/docs/getting-started/installation) for different platforms.
|
- Step-by-step [Installation Guide](https://netbird.io/docs/getting-started/installation) for different platforms.
|
||||||
* Web UI [repository](https://github.com/netbirdio/dashboard).
|
- Web UI [repository](https://github.com/netbirdio/dashboard).
|
||||||
* 5 min [demo video](https://youtu.be/Tu9tPsUWaY0) on YouTube.
|
- 5 min [demo video](https://youtu.be/Tu9tPsUWaY0) on YouTube.
|
||||||
|
|
||||||
|
|
||||||
### A bit on NetBird internals
|
### A bit on NetBird internals
|
||||||
* Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
|
- Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
|
||||||
* NetBird features [Management Service](management/) that holds network state, manages peer IPs, and distributes network updates to peers.
|
- Every agent connects to [Management Service](management/) that holds network state, manages peer IPs, and distributes network updates to agents (peers).
|
||||||
* Every agent is connected to Management Service.
|
- NetBird agent uses WebRTC ICE implemented in [pion/ice library](https://github.com/pion/ice) to discover connection candidates when establishing a peer-to-peer connection between machines.
|
||||||
* NetBird agent uses WebRTC ICE implemented in [pion/ice library](https://github.com/pion/ice) to discover connection candidates when establishing a peer-to-peer connection between machines.
|
- Connection candidates are discovered with a help of [STUN](https://en.wikipedia.org/wiki/STUN) servers.
|
||||||
* Connection candidates are discovered with a help of [STUN](https://en.wikipedia.org/wiki/STUN) server.
|
- Agents negotiate a connection through [Signal Service](signal/) passing p2p encrypted messages with candidates.
|
||||||
* Agents negotiate a connection through [Signal Service](signal/) passing p2p encrypted messages.
|
- Sometimes the NAT traversal is unsuccessful due to strict NATs (e.g. mobile carrier-grade NAT) and p2p connection isn't possible. When this occurs the system falls back to a relay server called [TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT), and a secure WireGuard tunnel is established via the TURN server.
|
||||||
* Signal Service uses public WireGuard keys to route messages between peers.
|
|
||||||
* Sometimes the NAT traversal is unsuccessful due to strict NATs (e.g. mobile carrier-grade NAT) and p2p connection isn't possible. When this occurs the system falls back to a relay server called [TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT), and a secure WireGuard tunnel is established via the TURN server.
|
|
||||||
|
|
||||||
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in NetBird setups.
|
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in NetBird setups.
|
||||||
|
|
||||||
@@ -107,9 +96,18 @@ See a complete [architecture overview](https://netbird.io/docs/overview/architec
|
|||||||
### Roadmap
|
### Roadmap
|
||||||
- [Public Roadmap](https://github.com/netbirdio/netbird/projects/2)
|
- [Public Roadmap](https://github.com/netbirdio/netbird/projects/2)
|
||||||
|
|
||||||
|
### Community projects
|
||||||
|
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird)
|
||||||
|
|
||||||
|
### Support acknowledgement
|
||||||
|
|
||||||
|
In November 2022, NetBird joined the [StartUpSecure program](https://www.forschung-it-sicherheit-kommunikationssysteme.de/foerderung/bekanntmachungen/startup-secure) sponsored by The Federal Ministry of Education and Research of The Federal Republic of Germany. Together with [CISPA Helmholtz Center for Information Security](https://cispa.de/en) NetBird brings the security best practices and simplicity to private networking.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### Testimonials
|
### Testimonials
|
||||||
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), and [Coturn](https://github.com/coturn/coturn). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
|
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), and [Coturn](https://github.com/coturn/coturn). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
|
||||||
|
|
||||||
### Legal
|
### Legal
|
||||||
[WireGuard](https://wireguard.com/) is a registered trademark of Jason A. Donenfeld.
|
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
FROM gcr.io/distroless/base:debug
|
FROM gcr.io/distroless/base:debug
|
||||||
ENV WT_LOG_FILE=console
|
ENV WT_LOG_FILE=console
|
||||||
|
ENV PATH=/sbin:/usr/sbin:/bin:/usr/bin:/busybox
|
||||||
|
SHELL ["/busybox/sh","-c"]
|
||||||
|
RUN sed -i -E 's/(^root:.+)\/sbin\/nologin/\1\/busybox\/sh/g' /etc/passwd
|
||||||
ENTRYPOINT [ "/go/bin/netbird","up"]
|
ENTRYPOINT [ "/go/bin/netbird","up"]
|
||||||
COPY netbird /go/bin/netbird
|
COPY netbird /go/bin/netbird
|
||||||
@@ -43,6 +43,8 @@ var loginCmd = &cobra.Command{
|
|||||||
return fmt.Errorf("get config file: %v", err)
|
return fmt.Errorf("get config file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
|
||||||
|
|
||||||
err = foregroundLogin(ctx, cmd, config, setupKey)
|
err = foregroundLogin(ctx, cmd, config, setupKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("foreground login failed: %v", err)
|
return fmt.Errorf("foreground login failed: %v", err)
|
||||||
@@ -167,7 +169,8 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
|
|||||||
hostedClient := internal.NewHostedDeviceFlow(
|
hostedClient := internal.NewHostedDeviceFlow(
|
||||||
providerConfig.ProviderConfig.Audience,
|
providerConfig.ProviderConfig.Audience,
|
||||||
providerConfig.ProviderConfig.ClientID,
|
providerConfig.ProviderConfig.ClientID,
|
||||||
providerConfig.ProviderConfig.Domain,
|
providerConfig.ProviderConfig.TokenEndpoint,
|
||||||
|
providerConfig.ProviderConfig.DeviceAuthEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
flowInfo, err := hostedClient.RequestDeviceCode(context.TODO())
|
flowInfo, err := hostedClient.RequestDeviceCode(context.TODO())
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ func init() {
|
|||||||
rootCmd.AddCommand(statusCmd)
|
rootCmd.AddCommand(statusCmd)
|
||||||
rootCmd.AddCommand(loginCmd)
|
rootCmd.AddCommand(loginCmd)
|
||||||
rootCmd.AddCommand(versionCmd)
|
rootCmd.AddCommand(versionCmd)
|
||||||
|
rootCmd.AddCommand(sshCmd)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ func newSVCConfig() *service.Config {
|
|||||||
Name: name,
|
Name: name,
|
||||||
DisplayName: "Netbird",
|
DisplayName: "Netbird",
|
||||||
Description: "A WireGuard-based mesh network that connects your devices into a single private network.",
|
Description: "A WireGuard-based mesh network that connects your devices into a single private network.",
|
||||||
|
Option: make(service.KeyValue),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -32,13 +34,34 @@ var installCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if managementURL != "" {
|
if managementURL != "" {
|
||||||
svcConfig.Arguments = append(svcConfig.Arguments, "--management-url")
|
svcConfig.Arguments = append(svcConfig.Arguments, "--management-url", managementURL)
|
||||||
svcConfig.Arguments = append(svcConfig.Arguments, managementURL)
|
}
|
||||||
|
|
||||||
|
if logFile != "console" {
|
||||||
|
svcConfig.Arguments = append(svcConfig.Arguments, "--log-file", logFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" {
|
||||||
// Respected only by systemd systems
|
// Respected only by systemd systems
|
||||||
svcConfig.Dependencies = []string{"After=network.target syslog.target"}
|
svcConfig.Dependencies = []string{"After=network.target syslog.target"}
|
||||||
|
|
||||||
|
if logFile != "console" {
|
||||||
|
setStdLogPath := true
|
||||||
|
dir := filepath.Dir(logFile)
|
||||||
|
|
||||||
|
_, err := os.Stat(dir)
|
||||||
|
if err != nil {
|
||||||
|
err = os.MkdirAll(dir, 0750)
|
||||||
|
if err != nil {
|
||||||
|
setStdLogPath = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if setStdLogPath {
|
||||||
|
svcConfig.Option["LogOutput"] = true
|
||||||
|
svcConfig.Option["LogDirectory"] = dir
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
|||||||
115
client/cmd/ssh.go
Normal file
115
client/cmd/ssh.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
port int
|
||||||
|
user = "root"
|
||||||
|
host string
|
||||||
|
)
|
||||||
|
|
||||||
|
var sshCmd = &cobra.Command{
|
||||||
|
Use: "ssh",
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return errors.New("requires a host argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
split := strings.Split(args[0], "@")
|
||||||
|
if len(split) == 2 {
|
||||||
|
user = split[0]
|
||||||
|
host = split[1]
|
||||||
|
} else {
|
||||||
|
host = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Short: "connect to a remote SSH server",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars()
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := util.InitLog(logLevel, "console")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !util.IsAdmin() {
|
||||||
|
cmd.Printf("error: you must have Administrator privileges to run this command\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := internal.CtxInitState(cmd.Context())
|
||||||
|
|
||||||
|
config, err := internal.ReadConfig("", "", configPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
sshctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// blocking
|
||||||
|
if err := runSSH(sshctx, host, []byte(config.SSHKey), cmd); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-sig:
|
||||||
|
cancel()
|
||||||
|
case <-sshctx.Done():
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSSH(ctx context.Context, addr string, pemKey []byte, cmd *cobra.Command) error {
|
||||||
|
c, err := nbssh.DialWithKey(fmt.Sprintf("%s:%d", addr, port), user, pemKey)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Printf("Error: %v\n", err)
|
||||||
|
cmd.Printf("Couldn't connect. " +
|
||||||
|
"You might be disconnected from the NetBird network, or the NetBird agent isn't running.\n" +
|
||||||
|
"Run the status command: \n\n" +
|
||||||
|
" netbird status\n\n" +
|
||||||
|
"It might also be that the SSH server is disabled on the agent you are trying to connect to.\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
err = c.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = c.OpenTerminal()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sshCmd.PersistentFlags().IntVarP(&port, "port", "p", nbssh.DefaultSSHPort, "Sets remote SSH port. Defaults to "+fmt.Sprint(nbssh.DefaultSSHPort))
|
||||||
|
}
|
||||||
@@ -3,13 +3,26 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
var (
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
detailFlag bool
|
||||||
|
ipv4Flag bool
|
||||||
|
ipsFilter []string
|
||||||
|
statusFilter string
|
||||||
|
ipsFilterMap map[string]struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
var statusCmd = &cobra.Command{
|
var statusCmd = &cobra.Command{
|
||||||
@@ -20,7 +33,12 @@ var statusCmd = &cobra.Command{
|
|||||||
|
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
err := util.InitLog(logLevel, "console")
|
err := parseFilters()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = util.InitLog(logLevel, "console")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed initializing log %v", err)
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
}
|
}
|
||||||
@@ -35,21 +53,267 @@ var statusCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
resp, err := proto.NewDaemonServiceClient(conn).Status(cmd.Context(), &proto.StatusRequest{})
|
resp, err := proto.NewDaemonServiceClient(conn).Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("status failed: %v", status.Convert(err).Message())
|
return fmt.Errorf("status failed: %v", status.Convert(err).Message())
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Printf("Status: %s\n\n", resp.GetStatus())
|
daemonStatus := fmt.Sprintf("Daemon status: %s\n", resp.GetStatus())
|
||||||
if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) {
|
if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) {
|
||||||
|
|
||||||
cmd.Printf("Run UP command to log in with SSO (interactive login):\n\n" +
|
cmd.Printf("%s\n"+
|
||||||
|
"Run UP command to log in with SSO (interactive login):\n\n"+
|
||||||
" netbird up \n\n"+
|
" netbird up \n\n"+
|
||||||
"If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n"+
|
"If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n"+
|
||||||
"you can use a setup-key:\n\n netbird up --management-url <YOUR_MANAGEMENT_URL> --setup-key <YOUR_SETUP_KEY>\n\n"+
|
"you can use a setup-key:\n\n netbird up --management-url <YOUR_MANAGEMENT_URL> --setup-key <YOUR_SETUP_KEY>\n\n"+
|
||||||
"More info: https://www.netbird.io/docs/overview/setup-keys\n\n")
|
"More info: https://www.netbird.io/docs/overview/setup-keys\n\n",
|
||||||
|
daemonStatus,
|
||||||
|
)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pbFullStatus := resp.GetFullStatus()
|
||||||
|
fullStatus := fromProtoFullStatus(pbFullStatus)
|
||||||
|
|
||||||
|
cmd.Print(parseFullStatus(fullStatus, detailFlag, daemonStatus, resp.GetDaemonVersion(), ipv4Flag))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ipsFilterMap = make(map[string]struct{})
|
||||||
|
statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information")
|
||||||
|
statusCmd.PersistentFlags().BoolVar(&ipv4Flag, "ipv4", false, "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33")
|
||||||
|
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200")
|
||||||
|
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFilters() error {
|
||||||
|
switch strings.ToLower(statusFilter) {
|
||||||
|
case "", "disconnected", "connected":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("wrong status filter, should be one of connected|disconnected, got: %s", statusFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ipsFilter) > 0 {
|
||||||
|
for _, addr := range ipsFilter {
|
||||||
|
_, err := netip.ParseAddr(addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got an invalid IP address in the filter: address %s, error %s", addr, err)
|
||||||
|
}
|
||||||
|
ipsFilterMap[addr] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromProtoFullStatus(pbFullStatus *proto.FullStatus) nbStatus.FullStatus {
|
||||||
|
var fullStatus nbStatus.FullStatus
|
||||||
|
managementState := pbFullStatus.GetManagementState()
|
||||||
|
fullStatus.ManagementState.URL = managementState.GetURL()
|
||||||
|
fullStatus.ManagementState.Connected = managementState.GetConnected()
|
||||||
|
|
||||||
|
signalState := pbFullStatus.GetSignalState()
|
||||||
|
fullStatus.SignalState.URL = signalState.GetURL()
|
||||||
|
fullStatus.SignalState.Connected = signalState.GetConnected()
|
||||||
|
|
||||||
|
localPeerState := pbFullStatus.GetLocalPeerState()
|
||||||
|
fullStatus.LocalPeerState.IP = localPeerState.GetIP()
|
||||||
|
fullStatus.LocalPeerState.PubKey = localPeerState.GetPubKey()
|
||||||
|
fullStatus.LocalPeerState.KernelInterface = localPeerState.GetKernelInterface()
|
||||||
|
fullStatus.LocalPeerState.FQDN = localPeerState.GetFqdn()
|
||||||
|
|
||||||
|
var peersState []nbStatus.PeerState
|
||||||
|
|
||||||
|
for _, pbPeerState := range pbFullStatus.GetPeers() {
|
||||||
|
timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local()
|
||||||
|
peerState := nbStatus.PeerState{
|
||||||
|
IP: pbPeerState.GetIP(),
|
||||||
|
PubKey: pbPeerState.GetPubKey(),
|
||||||
|
ConnStatus: pbPeerState.GetConnStatus(),
|
||||||
|
ConnStatusUpdate: timeLocal,
|
||||||
|
Relayed: pbPeerState.GetRelayed(),
|
||||||
|
Direct: pbPeerState.GetDirect(),
|
||||||
|
LocalIceCandidateType: pbPeerState.GetLocalIceCandidateType(),
|
||||||
|
RemoteIceCandidateType: pbPeerState.GetRemoteIceCandidateType(),
|
||||||
|
FQDN: pbPeerState.GetFqdn(),
|
||||||
|
}
|
||||||
|
peersState = append(peersState, peerState)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullStatus.Peers = peersState
|
||||||
|
|
||||||
|
return fullStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFullStatus(fullStatus nbStatus.FullStatus, printDetail bool, daemonStatus string, daemonVersion string, flag bool) string {
|
||||||
|
|
||||||
|
interfaceIP := fullStatus.LocalPeerState.IP
|
||||||
|
|
||||||
|
ip, _, err := net.ParseCIDR(interfaceIP)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipv4Flag {
|
||||||
|
return fmt.Sprintf("%s\n", ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
managementStatusURL = ""
|
||||||
|
signalStatusURL = ""
|
||||||
|
managementConnString = "Disconnected"
|
||||||
|
signalConnString = "Disconnected"
|
||||||
|
interfaceTypeString = "Userspace"
|
||||||
|
)
|
||||||
|
|
||||||
|
if printDetail {
|
||||||
|
managementStatusURL = fmt.Sprintf(" to %s", fullStatus.ManagementState.URL)
|
||||||
|
signalStatusURL = fmt.Sprintf(" to %s", fullStatus.SignalState.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fullStatus.ManagementState.Connected {
|
||||||
|
managementConnString = "Connected"
|
||||||
|
}
|
||||||
|
|
||||||
|
if fullStatus.SignalState.Connected {
|
||||||
|
signalConnString = "Connected"
|
||||||
|
}
|
||||||
|
|
||||||
|
if fullStatus.LocalPeerState.KernelInterface {
|
||||||
|
interfaceTypeString = "Kernel"
|
||||||
|
} else if fullStatus.LocalPeerState.IP == "" {
|
||||||
|
interfaceTypeString = "N/A"
|
||||||
|
interfaceIP = "N/A"
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedPeersString, peersConnected := parsePeers(fullStatus.Peers, printDetail)
|
||||||
|
|
||||||
|
peersCountString := fmt.Sprintf("%d/%d Connected", peersConnected, len(fullStatus.Peers))
|
||||||
|
|
||||||
|
summary := fmt.Sprintf(
|
||||||
|
"Daemon version: %s\n"+
|
||||||
|
"CLI version: %s\n"+
|
||||||
|
"%s"+ // daemon status
|
||||||
|
"Management: %s%s\n"+
|
||||||
|
"Signal: %s%s\n"+
|
||||||
|
"Domain: %s\n"+
|
||||||
|
"NetBird IP: %s\n"+
|
||||||
|
"Interface type: %s\n"+
|
||||||
|
"Peers count: %s\n",
|
||||||
|
daemonVersion,
|
||||||
|
system.NetbirdVersion(),
|
||||||
|
daemonStatus,
|
||||||
|
managementConnString,
|
||||||
|
managementStatusURL,
|
||||||
|
signalConnString,
|
||||||
|
signalStatusURL,
|
||||||
|
fullStatus.LocalPeerState.FQDN,
|
||||||
|
interfaceIP,
|
||||||
|
interfaceTypeString,
|
||||||
|
peersCountString,
|
||||||
|
)
|
||||||
|
|
||||||
|
if printDetail {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"Peers detail:"+
|
||||||
|
"%s\n"+
|
||||||
|
"%s",
|
||||||
|
parsedPeersString,
|
||||||
|
summary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePeers(peers []nbStatus.PeerState, printDetail bool) (string, int) {
|
||||||
|
var (
|
||||||
|
peersString = ""
|
||||||
|
peersConnected = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(peers) > 0 {
|
||||||
|
sort.SliceStable(peers, func(i, j int) bool {
|
||||||
|
iAddr, _ := netip.ParseAddr(peers[i].IP)
|
||||||
|
jAddr, _ := netip.ParseAddr(peers[j].IP)
|
||||||
|
return iAddr.Compare(jAddr) == -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedStatusString := peer.StatusConnected.String()
|
||||||
|
|
||||||
|
for _, peerState := range peers {
|
||||||
|
peerConnectionStatus := false
|
||||||
|
if peerState.ConnStatus == connectedStatusString {
|
||||||
|
peersConnected = peersConnected + 1
|
||||||
|
peerConnectionStatus = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if printDetail {
|
||||||
|
|
||||||
|
if skipDetailByFilters(peerState, peerConnectionStatus) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
localICE := "-"
|
||||||
|
remoteICE := "-"
|
||||||
|
connType := "-"
|
||||||
|
|
||||||
|
if peerConnectionStatus {
|
||||||
|
localICE = peerState.LocalIceCandidateType
|
||||||
|
remoteICE = peerState.RemoteIceCandidateType
|
||||||
|
connType = "P2P"
|
||||||
|
if peerState.Relayed {
|
||||||
|
connType = "Relayed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peerString := fmt.Sprintf(
|
||||||
|
"\n %s:\n"+
|
||||||
|
" NetBird IP: %s\n"+
|
||||||
|
" Public key: %s\n"+
|
||||||
|
" Status: %s\n"+
|
||||||
|
" -- detail --\n"+
|
||||||
|
" Connection type: %s\n"+
|
||||||
|
" Direct: %t\n"+
|
||||||
|
" ICE candidate (Local/Remote): %s/%s\n"+
|
||||||
|
" Last connection update: %s\n",
|
||||||
|
peerState.FQDN,
|
||||||
|
peerState.IP,
|
||||||
|
peerState.PubKey,
|
||||||
|
peerState.ConnStatus,
|
||||||
|
connType,
|
||||||
|
peerState.Direct,
|
||||||
|
localICE,
|
||||||
|
remoteICE,
|
||||||
|
peerState.ConnStatusUpdate.Format("2006-01-02 15:04:05"),
|
||||||
|
)
|
||||||
|
|
||||||
|
peersString = peersString + peerString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return peersString, peersConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipDetailByFilters(peerState nbStatus.PeerState, isConnected bool) bool {
|
||||||
|
statusEval := false
|
||||||
|
ipEval := false
|
||||||
|
|
||||||
|
if statusFilter != "" {
|
||||||
|
lowerStatusFilter := strings.ToLower(statusFilter)
|
||||||
|
if lowerStatusFilter == "disconnected" && isConnected {
|
||||||
|
statusEval = true
|
||||||
|
} else if lowerStatusFilter == "connected" && !isConnected {
|
||||||
|
statusEval = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ipsFilter) > 0 {
|
||||||
|
_, ok := ipsFilterMap[peerState.IP]
|
||||||
|
if !ok {
|
||||||
|
ipEval = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return statusEval || ipEval
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,18 +62,18 @@ 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.NewStore(config.Datadir)
|
store, err := mgmt.NewFileStore(config.Datadir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
peersUpdateManager := mgmt.NewPeersUpdateManager()
|
peersUpdateManager := mgmt.NewPeersUpdateManager()
|
||||||
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil)
|
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "")
|
||||||
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)
|
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -39,6 +40,8 @@ var upCmd = &cobra.Command{
|
|||||||
return fmt.Errorf("get config file: %v", err)
|
return fmt.Errorf("get config file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
|
||||||
|
|
||||||
err = foregroundLogin(ctx, cmd, config, setupKey)
|
err = foregroundLogin(ctx, cmd, config, setupKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("foreground login failed: %v", err)
|
return fmt.Errorf("foreground login failed: %v", err)
|
||||||
@@ -47,7 +50,7 @@ var upCmd = &cobra.Command{
|
|||||||
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)
|
return internal.RunClient(ctx, config, nbStatus.NewRecorder())
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ done:
|
|||||||
Pop $2
|
Pop $2
|
||||||
Exch $1
|
Exch $1
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
!macro GetAppFromCommand in out
|
!macro GetAppFromCommand in out
|
||||||
Push "${in}"
|
Push "${in}"
|
||||||
Call GetAppFromCommand
|
Call GetAppFromCommand
|
||||||
@@ -117,7 +118,7 @@ Call GetAppFromCommand ; Remove quotes and parameters from UninstCommand
|
|||||||
Pop $0
|
Pop $0
|
||||||
Pop $1
|
Pop $1
|
||||||
GetFullPathName $2 "$0\.."
|
GetFullPathName $2 "$0\.."
|
||||||
ExecWait '"$0" $1 _?=$2'
|
ExecWait '"$0" /S $1 _?=$2'
|
||||||
Delete "$0" ; Extra cleanup because we used _?=
|
Delete "$0" ; Extra cleanup because we used _?=
|
||||||
RMDir "$2"
|
RMDir "$2"
|
||||||
Pop $2
|
Pop $2
|
||||||
@@ -126,30 +127,27 @@ Pop $0
|
|||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
Function .onInit
|
Function .onInit
|
||||||
|
StrCpy $INSTDIR "${INSTALL_DIR}"
|
||||||
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Wiretrustee" "UninstallString"
|
|
||||||
${If} $R0 != ""
|
|
||||||
MessageBox MB_YESNO|MB_ICONQUESTION "Wiretrustee is installed. We must remove it before installing Netbird. Procced?" IDNO noWTUninstOld
|
|
||||||
!insertmacro UninstallPreviousNSIS $R0 "/NoMsgBox"
|
|
||||||
noWTUninstOld:
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
||||||
${If} $R0 != ""
|
${If} $R0 != ""
|
||||||
MessageBox MB_YESNO|MB_ICONQUESTION "$(^NAME) is already installed. Do you want to remove the previous version?" IDNO noUninstOld
|
# if silent install jump to uninstall step
|
||||||
|
IfSilent uninstall
|
||||||
|
|
||||||
|
MessageBox MB_YESNO|MB_ICONQUESTION "NetBird is already installed. We must remove it before installing upgrading NetBird. Proceed?" IDNO done IDYES uninstall
|
||||||
|
|
||||||
|
uninstall:
|
||||||
!insertmacro UninstallPreviousNSIS $R0 "/NoMsgBox"
|
!insertmacro UninstallPreviousNSIS $R0 "/NoMsgBox"
|
||||||
noUninstOld:
|
done:
|
||||||
|
|
||||||
${EndIf}
|
${EndIf}
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
######################################################################
|
######################################################################
|
||||||
Section -MainProgram
|
Section -MainProgram
|
||||||
${INSTALL_TYPE}
|
${INSTALL_TYPE}
|
||||||
SetOverwrite ifnewer
|
# SetOverwrite ifnewer
|
||||||
SetOutPath "$INSTDIR"
|
SetOutPath "$INSTDIR"
|
||||||
File /r "..\\dist\\netbird_windows_amd64\\"
|
File /r "..\\dist\\netbird_windows_amd64\\"
|
||||||
|
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
Section -Icons_Reg
|
Section -Icons_Reg
|
||||||
@@ -172,24 +170,29 @@ SetShellVarContext current
|
|||||||
CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
||||||
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section -Post
|
||||||
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service install'
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service install'
|
||||||
Exec '"$INSTDIR\${MAIN_APP_EXE}" service start'
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service start'
|
||||||
# sleep a bit for visibility
|
# sleep a bit for visibility
|
||||||
Sleep 1000
|
Sleep 1000
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
Section Uninstall
|
Section Uninstall
|
||||||
${INSTALL_TYPE}
|
${INSTALL_TYPE}
|
||||||
|
|
||||||
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service stop'
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service stop'
|
||||||
Exec '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
|
||||||
|
|
||||||
# kill ui client
|
# kill ui client
|
||||||
ExecWait `taskkill /im ${UI_APP_EXE}.exe`
|
ExecWait `taskkill /im ${UI_APP_EXE}.exe`
|
||||||
|
|
||||||
# wait the service uninstall take unblock the executable
|
# wait the service uninstall take unblock the executable
|
||||||
Sleep 3000
|
Sleep 3000
|
||||||
|
Delete "$INSTDIR\${UI_APP_EXE}"
|
||||||
|
Delete "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
RmDir /r "$INSTDIR"
|
RmDir /r "$INSTDIR"
|
||||||
|
|
||||||
SetShellVarContext current
|
SetShellVarContext current
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ package internal
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
mgm "github.com/netbirdio/netbird/management/client"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
mgm "github.com/netbirdio/netbird/management/client"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
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/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
var managementURLDefault *url.URL
|
var managementURLDefault *url.URL
|
||||||
@@ -22,7 +23,7 @@ func ManagementURLDefault() *url.URL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
managementURL, err := parseURL("Management URL", "https://api.wiretrustee.com:33073")
|
managementURL, err := ParseURL("Management URL", "https://api.wiretrustee.com:443")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -37,15 +38,47 @@ type Config struct {
|
|||||||
ManagementURL *url.URL
|
ManagementURL *url.URL
|
||||||
AdminURL *url.URL
|
AdminURL *url.URL
|
||||||
WgIface string
|
WgIface string
|
||||||
|
WgPort int
|
||||||
IFaceBlackList []string
|
IFaceBlackList []string
|
||||||
|
DisableIPv6Discovery bool
|
||||||
|
// SSHKey is a private SSH key in a PEM format
|
||||||
|
SSHKey string
|
||||||
|
|
||||||
|
// ExternalIP mappings, if different than the host interface IP
|
||||||
|
//
|
||||||
|
// External IP must not be behind a CGNAT and port-forwarding for incoming UDP packets from WgPort on ExternalIP
|
||||||
|
// to WgPort on host interface IP must be present. This can take form of single port-forwarding rule, 1:1 DNAT
|
||||||
|
// mapping ExternalIP to host interface IP, or a NAT DMZ to host interface IP.
|
||||||
|
//
|
||||||
|
// A single mapping will take the form of: external[/internal]
|
||||||
|
// external (required): either the external IP address or "stun" to use STUN to determine the external IP address
|
||||||
|
// internal (optional): either the internal/interface IP address or an interface name
|
||||||
|
//
|
||||||
|
// examples:
|
||||||
|
// "12.34.56.78" => all interfaces IPs will be mapped to external IP of 12.34.56.78
|
||||||
|
// "12.34.56.78/eth0" => IPv4 assigned to interface eth0 will be mapped to external IP of 12.34.56.78
|
||||||
|
// "12.34.56.78/10.1.2.3" => interface IP 10.1.2.3 will be mapped to external IP of 12.34.56.78
|
||||||
|
|
||||||
|
NATExternalIPs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(managementURL, adminURL, configPath, preSharedKey string) (*Config, error) {
|
func createNewConfig(managementURL, adminURL, configPath, preSharedKey string) (*Config, error) {
|
||||||
wgKey := generateKey()
|
wgKey := generateKey()
|
||||||
config := &Config{PrivateKey: wgKey, WgIface: iface.WgInterfaceDefault, IFaceBlackList: []string{}}
|
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config := &Config{
|
||||||
|
SSHKey: string(pem),
|
||||||
|
PrivateKey: wgKey,
|
||||||
|
WgIface: iface.WgInterfaceDefault,
|
||||||
|
WgPort: iface.DefaultWgPort,
|
||||||
|
IFaceBlackList: []string{},
|
||||||
|
DisableIPv6Discovery: false,
|
||||||
|
}
|
||||||
if managementURL != "" {
|
if managementURL != "" {
|
||||||
URL, err := parseURL("Management URL", managementURL)
|
URL, err := ParseURL("Management URL", managementURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -58,9 +91,18 @@ func createNewConfig(managementURL, adminURL, configPath, preSharedKey string) (
|
|||||||
config.PreSharedKey = preSharedKey
|
config.PreSharedKey = preSharedKey
|
||||||
}
|
}
|
||||||
|
|
||||||
config.IFaceBlackList = []string{iface.WgInterfaceDefault, "tun0"}
|
if adminURL != "" {
|
||||||
|
newURL, err := ParseURL("Admin Panel URL", adminURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.AdminURL = newURL
|
||||||
|
}
|
||||||
|
|
||||||
err := util.WriteJson(configPath, config)
|
config.IFaceBlackList = []string{iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "utun", "wg", "ts",
|
||||||
|
"Tailscale", "tailscale", "docker", "veth", "br-"}
|
||||||
|
|
||||||
|
err = util.WriteJson(configPath, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -68,7 +110,8 @@ func createNewConfig(managementURL, adminURL, configPath, preSharedKey string) (
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseURL(serviceName, managementURL string) (*url.URL, error) {
|
// ParseURL parses and validates management URL
|
||||||
|
func ParseURL(serviceName, managementURL string) (*url.URL, error) {
|
||||||
parsedMgmtURL, err := url.ParseRequestURI(managementURL)
|
parsedMgmtURL, err := url.ParseRequestURI(managementURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed parsing management URL %s: [%s]", managementURL, err.Error())
|
log.Errorf("failed parsing management URL %s: [%s]", managementURL, err.Error())
|
||||||
@@ -100,7 +143,7 @@ func ReadConfig(managementURL, adminURL, configPath string, preSharedKey *string
|
|||||||
if managementURL != "" && config.ManagementURL.String() != managementURL {
|
if managementURL != "" && config.ManagementURL.String() != managementURL {
|
||||||
log.Infof("new Management URL provided, updated to %s (old value %s)",
|
log.Infof("new Management URL provided, updated to %s (old value %s)",
|
||||||
managementURL, config.ManagementURL)
|
managementURL, config.ManagementURL)
|
||||||
newURL, err := parseURL("Management URL", managementURL)
|
newURL, err := ParseURL("Management URL", managementURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -111,7 +154,7 @@ func ReadConfig(managementURL, adminURL, configPath string, preSharedKey *string
|
|||||||
if adminURL != "" && (config.AdminURL == nil || config.AdminURL.String() != adminURL) {
|
if adminURL != "" && (config.AdminURL == nil || config.AdminURL.String() != adminURL) {
|
||||||
log.Infof("new Admin Panel URL provided, updated to %s (old value %s)",
|
log.Infof("new Admin Panel URL provided, updated to %s (old value %s)",
|
||||||
adminURL, config.AdminURL)
|
adminURL, config.AdminURL)
|
||||||
newURL, err := parseURL("Admin Panel URL", adminURL)
|
newURL, err := ParseURL("Admin Panel URL", adminURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -125,6 +168,19 @@ func ReadConfig(managementURL, adminURL, configPath string, preSharedKey *string
|
|||||||
config.PreSharedKey = *preSharedKey
|
config.PreSharedKey = *preSharedKey
|
||||||
refresh = true
|
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 refresh {
|
if refresh {
|
||||||
// since we have new management URL, we need to update config file
|
// since we have new management URL, we need to update config file
|
||||||
@@ -173,9 +229,14 @@ type ProviderConfig struct {
|
|||||||
// ClientSecret An IDP application client secret
|
// ClientSecret An IDP application client secret
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
// Domain An IDP API domain
|
// Domain An IDP API domain
|
||||||
|
// Deprecated. Use OIDCConfigEndpoint instead
|
||||||
Domain string
|
Domain string
|
||||||
// Audience An Audience for to authorization validation
|
// Audience An Audience for to authorization validation
|
||||||
Audience string
|
Audience string
|
||||||
|
// TokenEndpoint is the endpoint of an IDP manager where clients can obtain access token
|
||||||
|
TokenEndpoint string
|
||||||
|
// DeviceAuthEndpoint is the endpoint of an IDP manager where clients can obtain device authorization code
|
||||||
|
DeviceAuthEndpoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDeviceAuthorizationFlowInfo(ctx context.Context, config *Config) (DeviceAuthorizationFlow, error) {
|
func GetDeviceAuthorizationFlowInfo(ctx context.Context, config *Config) (DeviceAuthorizationFlow, error) {
|
||||||
@@ -197,7 +258,13 @@ func GetDeviceAuthorizationFlowInfo(ctx context.Context, config *Config) (Device
|
|||||||
log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), err)
|
log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), err)
|
||||||
return DeviceAuthorizationFlow{}, err
|
return DeviceAuthorizationFlow{}, err
|
||||||
}
|
}
|
||||||
log.Debugf("connected to management Service %s", config.ManagementURL.String())
|
log.Debugf("connected to the Management service %s", config.ManagementURL.String())
|
||||||
|
defer func() {
|
||||||
|
err = mgmClient.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to close the Management service client %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
serverKey, err := mgmClient.GetServerPublicKey()
|
serverKey, err := mgmClient.GetServerPublicKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -216,20 +283,40 @@ func GetDeviceAuthorizationFlowInfo(ctx context.Context, config *Config) (Device
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mgmClient.Close()
|
deviceAuthorizationFlow := DeviceAuthorizationFlow{
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed closing Management Service client: %v", err)
|
|
||||||
return DeviceAuthorizationFlow{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return DeviceAuthorizationFlow{
|
|
||||||
Provider: protoDeviceAuthorizationFlow.Provider.String(),
|
Provider: protoDeviceAuthorizationFlow.Provider.String(),
|
||||||
|
|
||||||
ProviderConfig: ProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
Audience: protoDeviceAuthorizationFlow.ProviderConfig.Audience,
|
Audience: protoDeviceAuthorizationFlow.GetProviderConfig().GetAudience(),
|
||||||
ClientID: protoDeviceAuthorizationFlow.ProviderConfig.ClientID,
|
ClientID: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientID(),
|
||||||
ClientSecret: protoDeviceAuthorizationFlow.ProviderConfig.ClientSecret,
|
ClientSecret: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientSecret(),
|
||||||
Domain: protoDeviceAuthorizationFlow.ProviderConfig.Domain,
|
Domain: protoDeviceAuthorizationFlow.GetProviderConfig().Domain,
|
||||||
|
TokenEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetTokenEndpoint(),
|
||||||
|
DeviceAuthEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetDeviceAuthEndpoint(),
|
||||||
},
|
},
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
err = isProviderConfigValid(deviceAuthorizationFlow.ProviderConfig)
|
||||||
|
if err != nil {
|
||||||
|
return DeviceAuthorizationFlow{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceAuthorizationFlow, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isProviderConfigValid(config ProviderConfig) error {
|
||||||
|
errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator"
|
||||||
|
if config.Audience == "" {
|
||||||
|
return fmt.Errorf(errorMSGFormat, "Audience")
|
||||||
|
}
|
||||||
|
if config.ClientID == "" {
|
||||||
|
return fmt.Errorf(errorMSGFormat, "Client ID")
|
||||||
|
}
|
||||||
|
if config.TokenEndpoint == "" {
|
||||||
|
return fmt.Errorf(errorMSGFormat, "Token Endpoint")
|
||||||
|
}
|
||||||
|
if config.DeviceAuthEndpoint == "" {
|
||||||
|
return fmt.Errorf(errorMSGFormat, "Device Auth Endpoint")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,13 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
|
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
@@ -15,17 +20,17 @@ import (
|
|||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
"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"
|
gstatus "google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunClient with main logic.
|
// RunClient with main logic.
|
||||||
func RunClient(ctx context.Context, config *Config) error {
|
func RunClient(ctx context.Context, config *Config, statusRecorder *nbStatus.Status) error {
|
||||||
backOff := &backoff.ExponentialBackOff{
|
backOff := &backoff.ExponentialBackOff{
|
||||||
InitialInterval: time.Second,
|
InitialInterval: time.Second,
|
||||||
RandomizationFactor: backoff.DefaultRandomizationFactor,
|
RandomizationFactor: 1,
|
||||||
Multiplier: backoff.DefaultMultiplier,
|
Multiplier: 1.7,
|
||||||
MaxInterval: 10 * time.Second,
|
MaxInterval: 15 * time.Second,
|
||||||
MaxElapsedTime: 24 * 3 * time.Hour, // stop the client after 3 days trying (must be a huge problem, e.g permission denied)
|
MaxElapsedTime: 3 * 30 * 24 * time.Hour, // 3 months
|
||||||
Stop: backoff.Stop,
|
Stop: backoff.Stop,
|
||||||
Clock: backoff.SystemClock,
|
Clock: backoff.SystemClock,
|
||||||
}
|
}
|
||||||
@@ -39,16 +44,6 @@ func RunClient(ctx context.Context, config *Config) error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
wrapErr := state.Wrap
|
wrapErr := state.Wrap
|
||||||
operation := func() error {
|
|
||||||
// if context cancelled we not start new backoff cycle
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
state.Set(StatusConnecting)
|
|
||||||
// validate our peer's Wireguard PRIVATE key
|
|
||||||
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
|
myPrivateKey, err := wgtypes.ParseKey(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]", config.PrivateKey, err.Error())
|
||||||
@@ -60,20 +55,72 @@ func RunClient(ctx context.Context, config *Config) error {
|
|||||||
mgmTlsEnabled = true
|
mgmTlsEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publicSSHKey, err := ssh.GeneratePublicKey([]byte(config.SSHKey))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
managementURL := config.ManagementURL.String()
|
||||||
|
statusRecorder.MarkManagementDisconnected(managementURL)
|
||||||
|
|
||||||
|
operation := func() error {
|
||||||
|
// if context cancelled we not start new backoff cycle
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Set(StatusConnecting)
|
||||||
|
|
||||||
engineCtx, cancel := context.WithCancel(ctx)
|
engineCtx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer func() {
|
||||||
|
statusRecorder.MarkManagementDisconnected(managementURL)
|
||||||
|
statusRecorder.CleanLocalPeerState()
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Debugf("conecting to the Management service %s", config.ManagementURL.Host)
|
||||||
|
mgmClient, err := mgm.NewClient(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
||||||
|
if err != nil {
|
||||||
|
return wrapErr(gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err))
|
||||||
|
}
|
||||||
|
log.Debugf("connected to the Management service %s", config.ManagementURL.Host)
|
||||||
|
defer func() {
|
||||||
|
err = mgmClient.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to close the Management service client %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// connect (just a connection, no stream yet) and login to Management Service to get an initial global Wiretrustee config
|
// connect (just a connection, no stream yet) and login to Management Service to get an initial global Wiretrustee config
|
||||||
mgmClient, loginResp, err := connectToManagement(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug(err)
|
log.Debug(err)
|
||||||
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
|
||||||
log.Info("peer registration required. Please run `netbird status` for details")
|
|
||||||
state.Set(StatusNeedsLogin)
|
state.Set(StatusNeedsLogin)
|
||||||
return nil
|
return backoff.Permanent(wrapErr(err)) // unrecoverable error
|
||||||
}
|
}
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
|
statusRecorder.MarkManagementConnected(managementURL)
|
||||||
|
|
||||||
|
localPeerState := nbStatus.LocalPeerState{
|
||||||
|
IP: loginResp.GetPeerConfig().GetAddress(),
|
||||||
|
PubKey: myPrivateKey.PublicKey().String(),
|
||||||
|
KernelInterface: iface.WireguardModuleIsLoaded(),
|
||||||
|
FQDN: loginResp.GetPeerConfig().GetFqdn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
statusRecorder.UpdateLocalPeerState(localPeerState)
|
||||||
|
|
||||||
|
signalURL := fmt.Sprintf("%s://%s",
|
||||||
|
strings.ToLower(loginResp.GetWiretrusteeConfig().GetSignal().GetProtocol().String()),
|
||||||
|
loginResp.GetWiretrusteeConfig().GetSignal().GetUri(),
|
||||||
|
)
|
||||||
|
|
||||||
|
statusRecorder.MarkSignalDisconnected(signalURL)
|
||||||
|
defer statusRecorder.MarkSignalDisconnected(signalURL)
|
||||||
|
|
||||||
// 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
|
||||||
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
|
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
|
||||||
@@ -81,6 +128,14 @@ func RunClient(ctx context.Context, config *Config) error {
|
|||||||
log.Error(err)
|
log.Error(err)
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
err = signalClient.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed closing Signal service client %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
statusRecorder.MarkSignalConnected(signalURL)
|
||||||
|
|
||||||
peerConfig := loginResp.GetPeerConfig()
|
peerConfig := loginResp.GetPeerConfig()
|
||||||
|
|
||||||
@@ -90,7 +145,7 @@ func RunClient(ctx context.Context, config *Config) error {
|
|||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig)
|
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig, statusRecorder)
|
||||||
err = engine.Start()
|
err = 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)
|
||||||
@@ -104,24 +159,13 @@ func RunClient(ctx context.Context, config *Config) error {
|
|||||||
|
|
||||||
backOff.Reset()
|
backOff.Reset()
|
||||||
|
|
||||||
err = mgmClient.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed closing Management Service client %v", err)
|
|
||||||
return wrapErr(err)
|
|
||||||
}
|
|
||||||
err = signalClient.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed closing Signal Service client %v", err)
|
|
||||||
return wrapErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = engine.Stop()
|
err = 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("stopped Netbird client")
|
log.Info("stopped NetBird client")
|
||||||
|
|
||||||
if _, err := state.Status(); err == ErrResetConnection {
|
if _, err := state.Status(); err == ErrResetConnection {
|
||||||
return err
|
return err
|
||||||
@@ -130,9 +174,9 @@ func RunClient(ctx context.Context, config *Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := backoff.Retry(operation, backOff)
|
err = backoff.Retry(operation, backOff)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("exiting client retry loop due to unrecoverable error: %s", err)
|
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -140,17 +184,16 @@ func RunClient(ctx context.Context, config *Config) error {
|
|||||||
|
|
||||||
// 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) {
|
||||||
iFaceBlackList := make(map[string]struct{})
|
|
||||||
for i := 0; i < len(config.IFaceBlackList); i += 2 {
|
|
||||||
iFaceBlackList[config.IFaceBlackList[i]] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
engineConf := &EngineConfig{
|
engineConf := &EngineConfig{
|
||||||
WgIfaceName: config.WgIface,
|
WgIfaceName: config.WgIface,
|
||||||
WgAddr: peerConfig.Address,
|
WgAddr: peerConfig.Address,
|
||||||
IFaceBlackList: iFaceBlackList,
|
IFaceBlackList: config.IFaceBlackList,
|
||||||
|
DisableIPv6Discovery: config.DisableIPv6Discovery,
|
||||||
WgPrivateKey: key,
|
WgPrivateKey: key,
|
||||||
WgPort: iface.DefaultWgPort,
|
WgPort: config.WgPort,
|
||||||
|
SSHKey: []byte(config.SSHKey),
|
||||||
|
NATExternalIPs: config.NATExternalIPs,
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.PreSharedKey != "" {
|
if config.PreSharedKey != "" {
|
||||||
@@ -176,33 +219,99 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig,
|
|||||||
signalClient, err := signal.NewClient(ctx, wtConfig.Signal.Uri, ourPrivateKey, sigTLSEnabled)
|
signalClient, err := signal.NewClient(ctx, wtConfig.Signal.Uri, ourPrivateKey, sigTLSEnabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error while connecting to the Signal Exchange Service %s: %s", wtConfig.Signal.Uri, err)
|
log.Errorf("error while connecting to the Signal Exchange Service %s: %s", wtConfig.Signal.Uri, err)
|
||||||
return nil, status.Errorf(codes.FailedPrecondition, "failed connecting to Signal Service : %s", err)
|
return nil, gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Signal Service : %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return signalClient, nil
|
return signalClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// connectToManagement creates Management Services client, establishes a connection, logs-in and gets a global Wiretrustee config (signal, turn, stun hosts, etc)
|
// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Wiretrustee config (signal, turn, stun hosts, etc)
|
||||||
func connectToManagement(ctx context.Context, managementAddr string, ourPrivateKey wgtypes.Key, tlsEnabled bool) (*mgm.GrpcClient, *mgmProto.LoginResponse, error) {
|
func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte) (*mgmProto.LoginResponse, error) {
|
||||||
log.Debugf("connecting to Management Service %s", managementAddr)
|
|
||||||
client, err := mgm.NewClient(ctx, managementAddr, ourPrivateKey, tlsEnabled)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, status.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err)
|
|
||||||
}
|
|
||||||
log.Debugf("connected to management server %s", managementAddr)
|
|
||||||
|
|
||||||
serverPublicKey, err := client.GetServerPublicKey()
|
serverPublicKey, err := client.GetServerPublicKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, status.Errorf(codes.FailedPrecondition, "failed while getting Management Service public key: %s", err)
|
return nil, gstatus.Errorf(codes.FailedPrecondition, "failed while getting Management Service public key: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sysInfo := system.GetInfo(ctx)
|
sysInfo := system.GetInfo(ctx)
|
||||||
loginResp, err := client.Login(*serverPublicKey, sysInfo)
|
loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("peer logged in to Management Service %s", managementAddr)
|
return loginResp, nil
|
||||||
|
}
|
||||||
return client, loginResp, nil
|
|
||||||
|
// ManagementLegacyPort is the port that was used before by the Management gRPC server.
|
||||||
|
// It is used for backward compatibility now.
|
||||||
|
// NB: hardcoded from github.com/netbirdio/netbird/management/cmd to avoid import
|
||||||
|
const ManagementLegacyPort = 33073
|
||||||
|
|
||||||
|
// UpdateOldManagementPort checks whether client can switch to the new Management port 443.
|
||||||
|
// If it can switch, then it updates the config and returns a new one. Otherwise, it returns the provided config.
|
||||||
|
// The check is performed only for the NetBird's managed version.
|
||||||
|
func UpdateOldManagementPort(ctx context.Context, config *Config, configPath string) (*Config, error) {
|
||||||
|
|
||||||
|
if config.ManagementURL.Hostname() != ManagementURLDefault().Hostname() {
|
||||||
|
// only do the check for the NetBird's managed version
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var mgmTlsEnabled bool
|
||||||
|
if config.ManagementURL.Scheme == "https" {
|
||||||
|
mgmTlsEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mgmTlsEnabled {
|
||||||
|
// only do the check for HTTPs scheme (the hosted version of the Management service is always HTTPs)
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if mgmTlsEnabled && config.ManagementURL.Port() == fmt.Sprintf("%d", ManagementLegacyPort) {
|
||||||
|
|
||||||
|
newURL, err := ParseURL("Management URL", fmt.Sprintf("%s://%s:%d",
|
||||||
|
config.ManagementURL.Scheme, config.ManagementURL.Hostname(), 443))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// here we check whether we could switch from the legacy 33073 port to the new 443
|
||||||
|
log.Infof("attempting to switch from the legacy Management URL %s to the new one %s",
|
||||||
|
config.ManagementURL.String(), newURL.String())
|
||||||
|
key, err := wgtypes.ParseKey(config.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("couldn't switch to the new Management %s", newURL.String())
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := mgm.NewClient(ctx, newURL.Host, key, mgmTlsEnabled)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("couldn't switch to the new Management %s", newURL.String())
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = client.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to close the Management service client %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// gRPC check
|
||||||
|
_, err = client.GetServerPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("couldn't switch to the new Management %s", newURL.String())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// everything is alright => update the config
|
||||||
|
newConfig, err := ReadConfig(newURL.String(), "", configPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("couldn't switch to the new Management %s", newURL.String())
|
||||||
|
return config, fmt.Errorf("failed updating config file: %v", err)
|
||||||
|
}
|
||||||
|
log.Infof("successfully switched to the new Management URL: %s", newURL.String())
|
||||||
|
|
||||||
|
return newConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|||||||
41
client/internal/dns/dbus_linux.go
Normal file
41
client/internal/dns/dbus_linux.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dbusDefaultFlag = 0
|
||||||
|
|
||||||
|
func isDbusListenerRunning(dest string, path dbus.ObjectPath) bool {
|
||||||
|
obj, closeConn, err := getDbusObject(dest, path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Store()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDbusObject(dest string, path dbus.ObjectPath) (dbus.BusObject, func(), error) {
|
||||||
|
conn, err := dbus.SystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
obj := conn.Object(dest, path)
|
||||||
|
|
||||||
|
closeFunc := func() {
|
||||||
|
closeErr := conn.Close()
|
||||||
|
if closeErr != nil {
|
||||||
|
log.Warnf("got an error closing dbus connection, err: %s", closeErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj, closeFunc, nil
|
||||||
|
}
|
||||||
154
client/internal/dns/file_linux.go
Normal file
154
client/internal/dns/file_linux.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fileGeneratedResolvConfContentHeader = "# Generated by NetBird"
|
||||||
|
fileGeneratedResolvConfSearchBeginContent = "search "
|
||||||
|
fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader +
|
||||||
|
"\n# If needed you can restore the original file by copying back %s\n\nnameserver %s\n" +
|
||||||
|
fileGeneratedResolvConfSearchBeginContent + "%s\n"
|
||||||
|
)
|
||||||
|
const (
|
||||||
|
fileDefaultResolvConfBackupLocation = defaultResolvConfPath + ".original.netbird"
|
||||||
|
fileMaxLineCharsLimit = 256
|
||||||
|
fileMaxNumberOfSearchDomains = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
var fileSearchLineBeginCharCount = len(fileGeneratedResolvConfSearchBeginContent)
|
||||||
|
|
||||||
|
type fileConfigurator struct {
|
||||||
|
originalPerms os.FileMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileConfigurator() (hostManager, error) {
|
||||||
|
return &fileConfigurator{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error {
|
||||||
|
backupFileExist := false
|
||||||
|
_, err := os.Stat(fileDefaultResolvConfBackupLocation)
|
||||||
|
if err == nil {
|
||||||
|
backupFileExist = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.routeAll {
|
||||||
|
if backupFileExist {
|
||||||
|
err = f.restore()
|
||||||
|
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: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unable to configure DNS for this peer using file manager without a Primary nameserver group")
|
||||||
|
}
|
||||||
|
managerType, err := getOSDNSManagerType()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch managerType {
|
||||||
|
case fileManager, netbirdManager:
|
||||||
|
if !backupFileExist {
|
||||||
|
err = f.backup()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to backup the resolv.conf file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// todo improve this and maybe restart DNS manager from scratch
|
||||||
|
return fmt.Errorf("something happened and file manager is not your prefered host dns configurator, restart the agent")
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchDomains string
|
||||||
|
appendedDomains := 0
|
||||||
|
for _, dConf := range config.domains {
|
||||||
|
if dConf.matchOnly {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if appendedDomains >= fileMaxNumberOfSearchDomains {
|
||||||
|
// lets log all skipped domains
|
||||||
|
log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, dConf.domain)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fileSearchLineBeginCharCount+len(searchDomains) > fileMaxLineCharsLimit {
|
||||||
|
// lets log all skipped domains
|
||||||
|
log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, dConf.domain)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
searchDomains += " " + dConf.domain
|
||||||
|
appendedDomains++
|
||||||
|
}
|
||||||
|
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains)
|
||||||
|
err = writeDNSConfig(content, defaultResolvConfPath, f.originalPerms)
|
||||||
|
if err != nil {
|
||||||
|
err = f.restore()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("attempt to restore default file failed with error: %s", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("created a NetBird managed %s file with your DNS settings", defaultResolvConfPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) restoreHostDNS() error {
|
||||||
|
return f.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) backup() error {
|
||||||
|
stats, err := os.Stat(defaultResolvConfPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got an error while checking stats for %s file. Error: %s", defaultResolvConfPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.originalPerms = stats.Mode()
|
||||||
|
|
||||||
|
err = copyFile(defaultResolvConfPath, fileDefaultResolvConfBackupLocation)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while backing up the %s file. Error: %s", defaultResolvConfPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) restore() error {
|
||||||
|
err := copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while restoring the %s file from %s. Error: %s", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.RemoveAll(fileDefaultResolvConfBackupLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeDNSConfig(content, fileName string, permissions os.FileMode) error {
|
||||||
|
log.Debugf("creating managed file %s", fileName)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(content)
|
||||||
|
err := os.WriteFile(fileName, buf.Bytes(), permissions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got an creating resolver file %s. Error: %s", fileName, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dest string) error {
|
||||||
|
stats, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got an error while checking stats for %s file when copying it. Error: %s", src, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesRead, err := os.ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got an error while reading the file %s file for copy. Error: %s", src, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(dest, bytesRead, stats.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got an writing the destination file %s for copy. Error: %s", dest, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
79
client/internal/dns/host.go
Normal file
79
client/internal/dns/host.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hostManager interface {
|
||||||
|
applyDNSConfig(config hostDNSConfig) error
|
||||||
|
restoreHostDNS() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type hostDNSConfig struct {
|
||||||
|
domains []domainConfig
|
||||||
|
routeAll bool
|
||||||
|
serverIP string
|
||||||
|
serverPort int
|
||||||
|
}
|
||||||
|
|
||||||
|
type domainConfig struct {
|
||||||
|
domain string
|
||||||
|
matchOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockHostConfigurator struct {
|
||||||
|
applyDNSConfigFunc func(config hostDNSConfig) error
|
||||||
|
restoreHostDNSFunc func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHostConfigurator) applyDNSConfig(config hostDNSConfig) error {
|
||||||
|
if m.applyDNSConfigFunc != nil {
|
||||||
|
return m.applyDNSConfigFunc(config)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("method applyDNSSettings is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHostConfigurator) restoreHostDNS() error {
|
||||||
|
if m.restoreHostDNSFunc != nil {
|
||||||
|
return m.restoreHostDNSFunc()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("method restoreHostDNS is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNoopHostMocker() hostManager {
|
||||||
|
return &mockHostConfigurator{
|
||||||
|
applyDNSConfigFunc: func(config hostDNSConfig) error { return nil },
|
||||||
|
restoreHostDNSFunc: func() error { return nil },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) hostDNSConfig {
|
||||||
|
config := hostDNSConfig{
|
||||||
|
routeAll: false,
|
||||||
|
serverIP: ip,
|
||||||
|
serverPort: port,
|
||||||
|
}
|
||||||
|
for _, nsConfig := range dnsConfig.NameServerGroups {
|
||||||
|
if nsConfig.Primary {
|
||||||
|
config.routeAll = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, domain := range nsConfig.Domains {
|
||||||
|
config.domains = append(config.domains, domainConfig{
|
||||||
|
domain: strings.TrimSuffix(domain, "."),
|
||||||
|
matchOnly: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, customZone := range dnsConfig.CustomZones {
|
||||||
|
config.domains = append(config.domains, domainConfig{
|
||||||
|
domain: strings.TrimSuffix(customZone.Domain, "."),
|
||||||
|
matchOnly: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
259
client/internal/dns/host_darwin.go
Normal file
259
client/internal/dns/host_darwin.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
netbirdDNSStateKeyFormat = "State:/Network/Service/NetBird-%s/DNS"
|
||||||
|
globalIPv4State = "State:/Network/Global/IPv4"
|
||||||
|
primaryServiceSetupKeyFormat = "Setup:/Network/Service/%s/DNS"
|
||||||
|
keySupplementalMatchDomains = "SupplementalMatchDomains"
|
||||||
|
keySupplementalMatchDomainsNoSearch = "SupplementalMatchDomainsNoSearch"
|
||||||
|
keyServerAddresses = "ServerAddresses"
|
||||||
|
keyServerPort = "ServerPort"
|
||||||
|
arraySymbol = "* "
|
||||||
|
digitSymbol = "# "
|
||||||
|
scutilPath = "/usr/sbin/scutil"
|
||||||
|
searchSuffix = "Search"
|
||||||
|
matchSuffix = "Match"
|
||||||
|
)
|
||||||
|
|
||||||
|
type systemConfigurator struct {
|
||||||
|
// primaryServiceID primary interface in the system. AKA the interface with the default route
|
||||||
|
primaryServiceID string
|
||||||
|
createdKeys map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHostManager(_ *iface.WGIface) (hostManager, error) {
|
||||||
|
return &systemConfigurator{
|
||||||
|
createdKeys: make(map[string]struct{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemConfigurator) applyDNSConfig(config hostDNSConfig) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if config.routeAll {
|
||||||
|
err = s.addDNSSetupForAll(config.serverIP, config.serverPort)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if s.primaryServiceID != "" {
|
||||||
|
err = s.removeKeyFromSystemConfig(getKeyWithInput(primaryServiceSetupKeyFormat, s.primaryServiceID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.primaryServiceID = ""
|
||||||
|
log.Infof("removed %s:%d as main DNS resolver for this peer", config.serverIP, config.serverPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
searchDomains []string
|
||||||
|
matchDomains []string
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, dConf := range config.domains {
|
||||||
|
if dConf.matchOnly {
|
||||||
|
matchDomains = append(matchDomains, dConf.domain)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
searchDomains = append(searchDomains, dConf.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
matchKey := getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)
|
||||||
|
if len(matchDomains) != 0 {
|
||||||
|
err = s.addMatchDomains(matchKey, strings.Join(matchDomains, " "), config.serverIP, config.serverPort)
|
||||||
|
} else {
|
||||||
|
log.Infof("removing match domains from the system")
|
||||||
|
err = s.removeKeyFromSystemConfig(matchKey)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
searchKey := getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix)
|
||||||
|
if len(searchDomains) != 0 {
|
||||||
|
err = s.addSearchDomains(searchKey, strings.Join(searchDomains, " "), config.serverIP, config.serverPort)
|
||||||
|
} else {
|
||||||
|
log.Infof("removing search domains from the system")
|
||||||
|
err = s.removeKeyFromSystemConfig(searchKey)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemConfigurator) restoreHostDNS() error {
|
||||||
|
lines := ""
|
||||||
|
for key := range s.createdKeys {
|
||||||
|
lines += buildRemoveKeyOperation(key)
|
||||||
|
keyType := "search"
|
||||||
|
if strings.Contains(key, matchSuffix) {
|
||||||
|
keyType = "match"
|
||||||
|
}
|
||||||
|
log.Infof("removing %s domains from system", keyType)
|
||||||
|
}
|
||||||
|
if s.primaryServiceID != "" {
|
||||||
|
lines += buildRemoveKeyOperation(getKeyWithInput(primaryServiceSetupKeyFormat, s.primaryServiceID))
|
||||||
|
log.Infof("restoring DNS resolver configuration for system")
|
||||||
|
}
|
||||||
|
_, err := runSystemConfigCommand(wrapCommand(lines))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("got an error while cleaning the system configuration: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error {
|
||||||
|
line := buildRemoveKeyOperation(key)
|
||||||
|
_, err := runSystemConfigCommand(wrapCommand(line))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.createdKeys, key)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemConfigurator) addSearchDomains(key, domains string, ip string, port int) error {
|
||||||
|
err := s.addDNSState(key, domains, ip, port, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("added %d search domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains)
|
||||||
|
|
||||||
|
s.createdKeys[key] = struct{}{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemConfigurator) addMatchDomains(key, domains, dnsServer string, port int) error {
|
||||||
|
err := s.addDNSState(key, domains, dnsServer, port, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("added %d match domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains)
|
||||||
|
|
||||||
|
s.createdKeys[key] = struct{}{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemConfigurator) addDNSState(state, domains, dnsServer string, port int, enableSearch bool) error {
|
||||||
|
noSearch := "1"
|
||||||
|
if enableSearch {
|
||||||
|
noSearch = "0"
|
||||||
|
}
|
||||||
|
lines := buildAddCommandLine(keySupplementalMatchDomains, arraySymbol+domains)
|
||||||
|
lines += buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+noSearch)
|
||||||
|
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer)
|
||||||
|
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
|
||||||
|
|
||||||
|
addDomainCommand := buildCreateStateWithOperation(state, lines)
|
||||||
|
stdinCommands := wrapCommand(addDomainCommand)
|
||||||
|
|
||||||
|
_, err := runSystemConfigCommand(stdinCommands)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while applying state for domains %s, error: %s", domains, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error {
|
||||||
|
primaryServiceKey := s.getPrimaryService()
|
||||||
|
if primaryServiceKey == "" {
|
||||||
|
return fmt.Errorf("couldn't find the primary service key")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("configured %s:%d as main DNS resolver for this peer", dnsServer, port)
|
||||||
|
s.primaryServiceID = primaryServiceKey
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemConfigurator) getPrimaryService() string {
|
||||||
|
line := buildCommandLine("show", globalIPv4State, "")
|
||||||
|
stdinCommands := wrapCommand(line)
|
||||||
|
b, err := runSystemConfigCommand(stdinCommands)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("got error while sending the command: ", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(b))
|
||||||
|
for scanner.Scan() {
|
||||||
|
text := scanner.Text()
|
||||||
|
if strings.Contains(text, "PrimaryService") {
|
||||||
|
return strings.TrimSpace(strings.Split(text, ":")[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int) error {
|
||||||
|
lines := buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+strconv.Itoa(0))
|
||||||
|
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer)
|
||||||
|
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
|
||||||
|
addDomainCommand := buildCreateStateWithOperation(setupKey, lines)
|
||||||
|
stdinCommands := wrapCommand(addDomainCommand)
|
||||||
|
_, err := runSystemConfigCommand(stdinCommands)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while applying dns setup, error: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeyWithInput(format, key string) string {
|
||||||
|
return fmt.Sprintf(format, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAddCommandLine(key, value string) string {
|
||||||
|
return buildCommandLine("d.add", key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCommandLine(action, key, value string) string {
|
||||||
|
return fmt.Sprintf("%s %s %s\n", action, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapCommand(commands string) string {
|
||||||
|
return fmt.Sprintf("open\n%s\nquit\n", commands)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRemoveKeyOperation(key string) string {
|
||||||
|
return fmt.Sprintf("remove %s\n", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCreateStateWithOperation(state, commands string) string {
|
||||||
|
return buildWriteStateOperation("set", state, commands)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildWriteStateOperation(operation, state, commands string) string {
|
||||||
|
return fmt.Sprintf("d.init\n%s %s\n%s\nset %s\n", operation, state, commands, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSystemConfigCommand(command string) ([]byte, error) {
|
||||||
|
cmd := exec.Command(scutilPath)
|
||||||
|
cmd.Stdin = strings.NewReader(command)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("got error while running system configuration command: \"%s\", error: %s", command, err)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
75
client/internal/dns/host_linux.go
Normal file
75
client/internal/dns/host_linux.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultResolvConfPath = "/etc/resolv.conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
netbirdManager osManagerType = iota
|
||||||
|
fileManager
|
||||||
|
networkManager
|
||||||
|
systemdManager
|
||||||
|
resolvConfManager
|
||||||
|
)
|
||||||
|
|
||||||
|
type osManagerType int
|
||||||
|
|
||||||
|
func newHostManager(wgInterface *iface.WGIface) (hostManager, error) {
|
||||||
|
osManager, err := getOSDNSManagerType()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("discovered mode is: %d", osManager)
|
||||||
|
switch osManager {
|
||||||
|
case networkManager:
|
||||||
|
return newNetworkManagerDbusConfigurator(wgInterface)
|
||||||
|
case systemdManager:
|
||||||
|
return newSystemdDbusConfigurator(wgInterface)
|
||||||
|
default:
|
||||||
|
return newFileConfigurator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOSDNSManagerType() (osManagerType, error) {
|
||||||
|
|
||||||
|
file, err := os.Open(defaultResolvConfPath)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("unable to open %s for checking owner, got error: %s", defaultResolvConfPath, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
text := scanner.Text()
|
||||||
|
if len(text) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if text[0] != '#' {
|
||||||
|
return fileManager, nil
|
||||||
|
}
|
||||||
|
if strings.Contains(text, fileGeneratedResolvConfContentHeader) {
|
||||||
|
return netbirdManager, nil
|
||||||
|
}
|
||||||
|
if strings.Contains(text, "NetworkManager") && isDbusListenerRunning(networkManagerDest, networkManagerDbusObjectNode) && isNetworkManagerSupported() {
|
||||||
|
log.Debugf("is nm running on supported v? %t", isNetworkManagerSupportedVersion())
|
||||||
|
return networkManager, nil
|
||||||
|
}
|
||||||
|
if strings.Contains(text, "systemd-resolved") && isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) {
|
||||||
|
return systemdManager, nil
|
||||||
|
}
|
||||||
|
if strings.Contains(text, "resolvconf") {
|
||||||
|
return resolvConfManager, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileManager, nil
|
||||||
|
}
|
||||||
260
client/internal/dns/host_windows.go
Normal file
260
client/internal/dns/host_windows.go
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dnsPolicyConfigMatchPath = "SYSTEM\\CurrentControlSet\\Services\\Dnscache\\Parameters\\DnsPolicyConfig\\NetBird-Match"
|
||||||
|
dnsPolicyConfigVersionKey = "Version"
|
||||||
|
dnsPolicyConfigVersionValue = 2
|
||||||
|
dnsPolicyConfigNameKey = "Name"
|
||||||
|
dnsPolicyConfigGenericDNSServersKey = "GenericDNSServers"
|
||||||
|
dnsPolicyConfigConfigOptionsKey = "ConfigOptions"
|
||||||
|
dnsPolicyConfigConfigOptionsValue = 0x8
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
interfaceConfigPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces"
|
||||||
|
interfaceConfigNameServerKey = "NameServer"
|
||||||
|
interfaceConfigSearchListKey = "SearchList"
|
||||||
|
tcpipParametersPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters"
|
||||||
|
)
|
||||||
|
|
||||||
|
type registryConfigurator struct {
|
||||||
|
guid string
|
||||||
|
routingAll bool
|
||||||
|
existingSearchDomains []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHostManager(wgInterface *iface.WGIface) (hostManager, error) {
|
||||||
|
guid, err := wgInterface.GetInterfaceGUIDString()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ®istryConfigurator{
|
||||||
|
guid: guid,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) applyDNSConfig(config hostDNSConfig) error {
|
||||||
|
var err error
|
||||||
|
if config.routeAll {
|
||||||
|
err = r.addDNSSetupForAll(config.serverIP)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if r.routingAll {
|
||||||
|
err = r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.routingAll = false
|
||||||
|
log.Infof("removed %s as main DNS forwarder for this peer", config.serverIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
searchDomains []string
|
||||||
|
matchDomains []string
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, dConf := range config.domains {
|
||||||
|
if !dConf.matchOnly {
|
||||||
|
searchDomains = append(searchDomains, dConf.domain)
|
||||||
|
}
|
||||||
|
matchDomains = append(matchDomains, "."+dConf.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(matchDomains) != 0 {
|
||||||
|
err = r.addDNSMatchPolicy(matchDomains, config.serverIP)
|
||||||
|
} else {
|
||||||
|
err = removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.updateSearchDomains(searchDomains)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) addDNSSetupForAll(ip string) error {
|
||||||
|
err := r.setInterfaceRegistryKeyStringValue(interfaceConfigNameServerKey, ip)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("adding dns setup for all failed with error: %s", err)
|
||||||
|
}
|
||||||
|
r.routingAll = true
|
||||||
|
log.Infof("configured %s:53 as main DNS forwarder for this peer", ip)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) error {
|
||||||
|
_, err := registry.OpenKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath, registry.QUERY_VALUE)
|
||||||
|
if err == nil {
|
||||||
|
err = registry.DeleteKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %s", dnsPolicyConfigMatchPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
regKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath, registry.SET_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %s", dnsPolicyConfigMatchPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = regKey.SetDWordValue(dnsPolicyConfigVersionKey, dnsPolicyConfigVersionValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigVersionKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = regKey.SetStringsValue(dnsPolicyConfigNameKey, domains)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigNameKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigGenericDNSServersKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = regKey.SetDWordValue(dnsPolicyConfigConfigOptionsKey, dnsPolicyConfigConfigOptionsValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigConfigOptionsKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("added %d match domains to the state. Domain list: %s", len(domains), domains)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) restoreHostDNS() error {
|
||||||
|
err := removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.updateSearchDomains([]string{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) updateSearchDomains(domains []string) error {
|
||||||
|
value, err := getLocalMachineRegistryKeyStringValue(tcpipParametersPath, interfaceConfigSearchListKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get current search domains failed with error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
valueList := strings.Split(value, ",")
|
||||||
|
setExisting := false
|
||||||
|
if len(r.existingSearchDomains) == 0 {
|
||||||
|
r.existingSearchDomains = valueList
|
||||||
|
setExisting = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(domains) == 0 && setExisting {
|
||||||
|
log.Infof("added %d search domains to the registry. Domain list: %s", len(domains), domains)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newList := append(r.existingSearchDomains, domains...)
|
||||||
|
|
||||||
|
err = setLocalMachineRegistryKeyStringValue(tcpipParametersPath, interfaceConfigSearchListKey, strings.Join(newList, ","))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("adding search domain failed with error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("updated the search domains in the registry with %d domains. Domain list: %s", len(domains), domains)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) setInterfaceRegistryKeyStringValue(key, value string) error {
|
||||||
|
regKey, err := r.getInterfaceRegistryKey()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer regKey.Close()
|
||||||
|
|
||||||
|
err = regKey.SetStringValue(key, value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("applying key %s with value \"%s\" for interface failed with error: %s", key, value, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) deleteInterfaceRegistryKeyProperty(propertyKey string) error {
|
||||||
|
regKey, err := r.getInterfaceRegistryKey()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer regKey.Close()
|
||||||
|
|
||||||
|
err = regKey.DeleteValue(propertyKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("deleting registry key %s for interface failed with error: %s", propertyKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) getInterfaceRegistryKey() (registry.Key, error) {
|
||||||
|
var regKey registry.Key
|
||||||
|
|
||||||
|
regKeyPath := interfaceConfigPath + "\\" + r.guid
|
||||||
|
|
||||||
|
regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.SET_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return regKey, fmt.Errorf("unable to open the interface registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %s", regKeyPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return regKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeRegistryKeyFromDNSPolicyConfig(regKeyPath string) error {
|
||||||
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.QUERY_VALUE)
|
||||||
|
if err == nil {
|
||||||
|
k.Close()
|
||||||
|
err = registry.DeleteKey(registry.LOCAL_MACHINE, regKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %s", regKeyPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLocalMachineRegistryKeyStringValue(keyPath, key string) (string, error) {
|
||||||
|
regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to open existing key from registry, key path: HKEY_LOCAL_MACHINE\\%s, error: %s", keyPath, err)
|
||||||
|
}
|
||||||
|
defer regKey.Close()
|
||||||
|
|
||||||
|
val, _, err := regKey.GetStringValue(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("getting %s value for key path HKEY_LOCAL_MACHINE\\%s failed with error: %s", key, keyPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setLocalMachineRegistryKeyStringValue(keyPath, key, value string) error {
|
||||||
|
regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.SET_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to open existing key from registry, key path: HKEY_LOCAL_MACHINE\\%s, error: %s", keyPath, err)
|
||||||
|
}
|
||||||
|
defer regKey.Close()
|
||||||
|
|
||||||
|
err = regKey.SetStringValue(key, value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting %s value %s for key path HKEY_LOCAL_MACHINE\\%s failed with error: %s", key, value, keyPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
66
client/internal/dns/local.go
Normal file
66
client/internal/dns/local.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type localResolver struct {
|
||||||
|
registeredMap registrationMap
|
||||||
|
records sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeDNS handles a DNS request
|
||||||
|
func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
log.Tracef("received question: %#v\n", r.Question[0])
|
||||||
|
replyMessage := &dns.Msg{}
|
||||||
|
replyMessage.SetReply(r)
|
||||||
|
replyMessage.RecursionAvailable = true
|
||||||
|
replyMessage.Rcode = dns.RcodeSuccess
|
||||||
|
|
||||||
|
response := d.lookupRecord(r)
|
||||||
|
if response != nil {
|
||||||
|
replyMessage.Answer = append(replyMessage.Answer, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := w.WriteMsg(replyMessage)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("got an error while writing the local resolver response, error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *localResolver) lookupRecord(r *dns.Msg) dns.RR {
|
||||||
|
question := r.Question[0]
|
||||||
|
record, found := d.records.Load(buildRecordKey(question.Name, question.Qclass, question.Qtype))
|
||||||
|
if !found {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return record.(dns.RR)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *localResolver) registerRecord(record nbdns.SimpleRecord) error {
|
||||||
|
fullRecord, err := dns.NewRR(record.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fullRecord.Header().Rdlength = record.Len()
|
||||||
|
|
||||||
|
header := fullRecord.Header()
|
||||||
|
d.records.Store(buildRecordKey(header.Name, header.Class, header.Rrtype), fullRecord)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *localResolver) deleteRecord(recordKey string) {
|
||||||
|
d.records.Delete(dns.Fqdn(recordKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRecordKey(name string, class, qType uint16) string {
|
||||||
|
key := fmt.Sprintf("%s_%d_%d", name, class, qType)
|
||||||
|
return key
|
||||||
|
}
|
||||||
86
client/internal/dns/local_test.go
Normal file
86
client/internal/dns/local_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLocalResolver_ServeDNS(t *testing.T) {
|
||||||
|
recordA := nbdns.SimpleRecord{
|
||||||
|
Name: "peera.netbird.cloud.",
|
||||||
|
Type: 1,
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 300,
|
||||||
|
RData: "1.2.3.4",
|
||||||
|
}
|
||||||
|
|
||||||
|
recordCNAME := nbdns.SimpleRecord{
|
||||||
|
Name: "peerb.netbird.cloud.",
|
||||||
|
Type: 5,
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 300,
|
||||||
|
RData: "www.netbird.io",
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
inputRecord nbdns.SimpleRecord
|
||||||
|
inputMSG *dns.Msg
|
||||||
|
responseShouldBeNil bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Should Resolve A Record",
|
||||||
|
inputRecord: recordA,
|
||||||
|
inputMSG: new(dns.Msg).SetQuestion(recordA.Name, dns.TypeA),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Resolve CNAME Record",
|
||||||
|
inputRecord: recordCNAME,
|
||||||
|
inputMSG: new(dns.Msg).SetQuestion(recordCNAME.Name, dns.TypeCNAME),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Write When Not Found A Record",
|
||||||
|
inputRecord: recordA,
|
||||||
|
inputMSG: new(dns.Msg).SetQuestion("not.found.com", dns.TypeA),
|
||||||
|
responseShouldBeNil: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
resolver := &localResolver{
|
||||||
|
registeredMap: make(registrationMap),
|
||||||
|
}
|
||||||
|
_ = resolver.registerRecord(testCase.inputRecord)
|
||||||
|
var responseMSG *dns.Msg
|
||||||
|
responseWriter := &mockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
responseMSG = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver.ServeDNS(responseWriter, testCase.inputMSG)
|
||||||
|
|
||||||
|
if responseMSG == nil || len(responseMSG.Answer) == 0 {
|
||||||
|
if testCase.responseShouldBeNil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("should write a response message")
|
||||||
|
}
|
||||||
|
|
||||||
|
answerString := responseMSG.Answer[0].String()
|
||||||
|
if !strings.Contains(answerString, testCase.inputRecord.Name) {
|
||||||
|
t.Fatalf("answer doesn't contain the same domain name: \nWant: %s\nGot:%s", testCase.name, answerString)
|
||||||
|
}
|
||||||
|
if !strings.Contains(answerString, dns.Type(testCase.inputRecord.Type).String()) {
|
||||||
|
t.Fatalf("answer doesn't contain the correct type: \nWant: %s\nGot:%s", dns.Type(testCase.inputRecord.Type).String(), answerString)
|
||||||
|
}
|
||||||
|
if !strings.Contains(answerString, testCase.inputRecord.RData) {
|
||||||
|
t.Fatalf("answer doesn't contain the same address: \nWant: %s\nGot:%s", testCase.inputRecord.RData, answerString)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
35
client/internal/dns/mockServer.go
Normal file
35
client/internal/dns/mockServer.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockServer is the mock instance of a dns server
|
||||||
|
type MockServer struct {
|
||||||
|
StartFunc func()
|
||||||
|
StopFunc func()
|
||||||
|
UpdateDNSServerFunc func(serial uint64, update nbdns.Config) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start mock implementation of Start from Server interface
|
||||||
|
func (m *MockServer) Start() {
|
||||||
|
if m.StartFunc != nil {
|
||||||
|
m.StartFunc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop mock implementation of Stop from Server interface
|
||||||
|
func (m *MockServer) Stop() {
|
||||||
|
if m.StopFunc != nil {
|
||||||
|
m.StopFunc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDNSServer mock implementation of UpdateDNSServer from Server interface
|
||||||
|
func (m *MockServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
|
||||||
|
if m.UpdateDNSServerFunc != nil {
|
||||||
|
return m.UpdateDNSServerFunc(serial, update)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("method UpdateDNSServer is not implemented")
|
||||||
|
}
|
||||||
25
client/internal/dns/mock_test.go
Normal file
25
client/internal/dns/mock_test.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockResponseWriter struct {
|
||||||
|
WriteMsgFunc func(m *dns.Msg) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *mockResponseWriter) WriteMsg(m *dns.Msg) error {
|
||||||
|
if rw.WriteMsgFunc != nil {
|
||||||
|
return rw.WriteMsgFunc(m)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *mockResponseWriter) LocalAddr() net.Addr { return nil }
|
||||||
|
func (rw *mockResponseWriter) RemoteAddr() net.Addr { return nil }
|
||||||
|
func (rw *mockResponseWriter) Write([]byte) (int, error) { return 0, nil }
|
||||||
|
func (rw *mockResponseWriter) Close() error { return nil }
|
||||||
|
func (rw *mockResponseWriter) TsigStatus() error { return nil }
|
||||||
|
func (rw *mockResponseWriter) TsigTimersOnly(bool) {}
|
||||||
|
func (rw *mockResponseWriter) Hijack() {}
|
||||||
295
client/internal/dns/network_manager_linux.go
Normal file
295
client/internal/dns/network_manager_linux.go
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
"github.com/hashicorp/go-version"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net/netip"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
networkManagerDest = "org.freedesktop.NetworkManager"
|
||||||
|
networkManagerDbusObjectNode = "/org/freedesktop/NetworkManager"
|
||||||
|
networkManagerDbusDNSManagerInterface = "org.freedesktop.NetworkManager.DnsManager"
|
||||||
|
networkManagerDbusDNSManagerObjectNode = networkManagerDbusObjectNode + "/DnsManager"
|
||||||
|
networkManagerDbusDNSManagerModeProperty = networkManagerDbusDNSManagerInterface + ".Mode"
|
||||||
|
networkManagerDbusDNSManagerRcManagerProperty = networkManagerDbusDNSManagerInterface + ".RcManager"
|
||||||
|
networkManagerDbusVersionProperty = "org.freedesktop.NetworkManager.Version"
|
||||||
|
networkManagerDbusGetDeviceByIPIfaceMethod = networkManagerDest + ".GetDeviceByIpIface"
|
||||||
|
networkManagerDbusDeviceInterface = "org.freedesktop.NetworkManager.Device"
|
||||||
|
networkManagerDbusDeviceGetAppliedConnectionMethod = networkManagerDbusDeviceInterface + ".GetAppliedConnection"
|
||||||
|
networkManagerDbusDeviceReapplyMethod = networkManagerDbusDeviceInterface + ".Reapply"
|
||||||
|
networkManagerDbusDeviceDeleteMethod = networkManagerDbusDeviceInterface + ".Delete"
|
||||||
|
networkManagerDbusDefaultBehaviorFlag networkManagerConfigBehavior = 0
|
||||||
|
networkManagerDbusIPv4Key = "ipv4"
|
||||||
|
networkManagerDbusIPv6Key = "ipv6"
|
||||||
|
networkManagerDbusDNSKey = "dns"
|
||||||
|
networkManagerDbusDNSSearchKey = "dns-search"
|
||||||
|
networkManagerDbusDNSPriorityKey = "dns-priority"
|
||||||
|
|
||||||
|
// dns priority doc https://wiki.gnome.org/Projects/NetworkManager/DNS
|
||||||
|
networkManagerDbusPrimaryDNSPriority int32 = -500
|
||||||
|
networkManagerDbusWithMatchDomainPriority int32 = 0
|
||||||
|
networkManagerDbusSearchDomainOnlyPriority int32 = 50
|
||||||
|
supportedNetworkManagerVersionConstraint = ">= 1.16, < 1.28"
|
||||||
|
)
|
||||||
|
|
||||||
|
type networkManagerDbusConfigurator struct {
|
||||||
|
dbusLinkObject dbus.ObjectPath
|
||||||
|
routingAll bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// the types below are based on dbus specification, each field is mapped to a dbus type
|
||||||
|
// see https://dbus.freedesktop.org/doc/dbus-specification.html#basic-types for more details on dbus types
|
||||||
|
// see https://networkmanager.dev/docs/api/latest/gdbus-org.freedesktop.NetworkManager.Device.html on Network Manager input types
|
||||||
|
|
||||||
|
// networkManagerConnSettings maps to a (a{sa{sv}}) dbus output from GetAppliedConnection and input for Reapply methods
|
||||||
|
type networkManagerConnSettings map[string]map[string]dbus.Variant
|
||||||
|
|
||||||
|
// networkManagerConfigVersion maps to a (t) dbus output from GetAppliedConnection and input for Reapply methods
|
||||||
|
type networkManagerConfigVersion uint64
|
||||||
|
|
||||||
|
// networkManagerConfigBehavior maps to a (u) dbus input for GetAppliedConnection and Reapply methods
|
||||||
|
type networkManagerConfigBehavior uint32
|
||||||
|
|
||||||
|
// cleanDeprecatedSettings cleans deprecated settings that still returned by
|
||||||
|
// the GetAppliedConnection methods but can't be reApplied
|
||||||
|
func (s networkManagerConnSettings) cleanDeprecatedSettings() {
|
||||||
|
for _, key := range []string{"addresses", "routes"} {
|
||||||
|
delete(s[networkManagerDbusIPv4Key], key)
|
||||||
|
delete(s[networkManagerDbusIPv6Key], key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNetworkManagerDbusConfigurator(wgInterface *iface.WGIface) (hostManager, error) {
|
||||||
|
obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusObjectNode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
var s string
|
||||||
|
err = obj.Call(networkManagerDbusGetDeviceByIPIfaceMethod, dbusDefaultFlag, wgInterface.GetName()).Store(&s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("got network manager dbus Link Object: %s from net interface %s", s, wgInterface.GetName())
|
||||||
|
|
||||||
|
return &networkManagerDbusConfigurator{
|
||||||
|
dbusLinkObject: dbus.ObjectPath(s),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *networkManagerDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
|
||||||
|
connSettings, configVersion, err := n.getAppliedConnectionSettings()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got an error while retrieving the applied connection settings, error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
connSettings.cleanDeprecatedSettings()
|
||||||
|
|
||||||
|
dnsIP := netip.MustParseAddr(config.serverIP)
|
||||||
|
convDNSIP := binary.LittleEndian.Uint32(dnsIP.AsSlice())
|
||||||
|
connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSKey] = dbus.MakeVariant([]uint32{convDNSIP})
|
||||||
|
var (
|
||||||
|
searchDomains []string
|
||||||
|
matchDomains []string
|
||||||
|
)
|
||||||
|
for _, dConf := range config.domains {
|
||||||
|
if dConf.matchOnly {
|
||||||
|
matchDomains = append(matchDomains, "~."+dns.Fqdn(dConf.domain))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
searchDomains = append(searchDomains, dns.Fqdn(dConf.domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
newDomainList := append(searchDomains, matchDomains...)
|
||||||
|
|
||||||
|
priority := networkManagerDbusSearchDomainOnlyPriority
|
||||||
|
switch {
|
||||||
|
case config.routeAll:
|
||||||
|
priority = networkManagerDbusPrimaryDNSPriority
|
||||||
|
newDomainList = append(newDomainList, "~.")
|
||||||
|
if !n.routingAll {
|
||||||
|
log.Infof("configured %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
|
||||||
|
}
|
||||||
|
case len(matchDomains) > 0:
|
||||||
|
priority = networkManagerDbusWithMatchDomainPriority
|
||||||
|
}
|
||||||
|
|
||||||
|
if priority != networkManagerDbusPrimaryDNSPriority && n.routingAll {
|
||||||
|
log.Infof("removing %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
|
||||||
|
n.routingAll = false
|
||||||
|
}
|
||||||
|
|
||||||
|
connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSPriorityKey] = dbus.MakeVariant(priority)
|
||||||
|
connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSSearchKey] = dbus.MakeVariant(newDomainList)
|
||||||
|
|
||||||
|
log.Infof("adding %d search domains and %d match domains. Search list: %s , Match list: %s", len(searchDomains), len(matchDomains), searchDomains, matchDomains)
|
||||||
|
err = n.reApplyConnectionSettings(connSettings, configVersion)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got an error while reapplying the connection with new settings, error: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *networkManagerDbusConfigurator) restoreHostDNS() error {
|
||||||
|
// once the interface is gone network manager cleans all config associated with it
|
||||||
|
return n.deleteConnectionSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *networkManagerDbusConfigurator) getAppliedConnectionSettings() (networkManagerConnSettings, networkManagerConfigVersion, error) {
|
||||||
|
obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("got error while attempting to retrieve the applied connection settings, err: %s", err)
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
connSettings networkManagerConnSettings
|
||||||
|
configVersion networkManagerConfigVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
err = obj.CallWithContext(ctx, networkManagerDbusDeviceGetAppliedConnectionMethod, dbusDefaultFlag,
|
||||||
|
networkManagerDbusDefaultBehaviorFlag).Store(&connSettings, &configVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("got error while calling GetAppliedConnection method with context, err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return connSettings, configVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *networkManagerDbusConfigurator) reApplyConnectionSettings(connSettings networkManagerConnSettings, configVersion networkManagerConfigVersion) error {
|
||||||
|
obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while attempting to retrieve the applied connection settings, err: %s", err)
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = obj.CallWithContext(ctx, networkManagerDbusDeviceReapplyMethod, dbusDefaultFlag,
|
||||||
|
connSettings, configVersion, networkManagerDbusDefaultBehaviorFlag).Store()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while calling ReApply method with context, err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *networkManagerDbusConfigurator) deleteConnectionSettings() error {
|
||||||
|
obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while attempting to retrieve the applied connection settings, err: %s", err)
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = obj.CallWithContext(ctx, networkManagerDbusDeviceDeleteMethod, dbusDefaultFlag).Store()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while calling delete method with context, err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNetworkManagerSupported() bool {
|
||||||
|
return isNetworkManagerSupportedVersion() && isNetworkManagerSupportedMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNetworkManagerSupportedMode() bool {
|
||||||
|
var mode string
|
||||||
|
err := getNetworkManagerDNSProperty(networkManagerDbusDNSManagerModeProperty, &mode)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch mode {
|
||||||
|
case "dnsmasq", "unbound", "systemd-resolved":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
var rcManager string
|
||||||
|
err = getNetworkManagerDNSProperty(networkManagerDbusDNSManagerRcManagerProperty, &rcManager)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if rcManager == "unmanaged" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNetworkManagerDNSProperty(property string, store any) error {
|
||||||
|
obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusDNSManagerObjectNode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while attempting to retrieve the network manager dns manager object, error: %s", err)
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
|
||||||
|
v, e := obj.GetProperty(property)
|
||||||
|
if e != nil {
|
||||||
|
return fmt.Errorf("got an error getting property %s: %v", property, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.Store(store)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNetworkManagerSupportedVersion() bool {
|
||||||
|
obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusObjectNode)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("got error while attempting to get the network manager object, err: %s", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
defer closeConn()
|
||||||
|
|
||||||
|
value, err := obj.GetProperty(networkManagerDbusVersionProperty)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to retrieve network manager mode, got error: %s", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
versionValue, err := parseVersion(value.Value().(string))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
constraints, err := version.NewConstraint(supportedNetworkManagerVersionConstraint)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return constraints.Check(versionValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseVersion(inputVersion string) (*version.Version, error) {
|
||||||
|
reg, err := regexp.Compile(version.SemverRegexpRaw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if inputVersion == "" || !reg.MatchString(inputVersion) {
|
||||||
|
return nil, fmt.Errorf("couldn't parse the provided version: Not SemVer")
|
||||||
|
}
|
||||||
|
|
||||||
|
verObj, err := version.NewVersion(inputVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return verObj, nil
|
||||||
|
}
|
||||||
333
client/internal/dns/server.go
Normal file
333
client/internal/dns/server.go
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
port = 53
|
||||||
|
customPort = 5053
|
||||||
|
defaultIP = "127.0.0.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server is a dns server interface
|
||||||
|
type Server interface {
|
||||||
|
Start()
|
||||||
|
Stop()
|
||||||
|
UpdateDNSServer(serial uint64, update nbdns.Config) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultServer dns server object
|
||||||
|
type DefaultServer struct {
|
||||||
|
ctx context.Context
|
||||||
|
stop context.CancelFunc
|
||||||
|
mux sync.Mutex
|
||||||
|
server *dns.Server
|
||||||
|
dnsMux *dns.ServeMux
|
||||||
|
dnsMuxMap registrationMap
|
||||||
|
localResolver *localResolver
|
||||||
|
wgInterface *iface.WGIface
|
||||||
|
hostManager hostManager
|
||||||
|
updateSerial uint64
|
||||||
|
listenerIsRunning bool
|
||||||
|
runtimePort int
|
||||||
|
runtimeIP string
|
||||||
|
}
|
||||||
|
|
||||||
|
type registrationMap map[string]struct{}
|
||||||
|
|
||||||
|
type muxUpdate struct {
|
||||||
|
domain string
|
||||||
|
handler dns.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultServer returns a new dns server
|
||||||
|
func NewDefaultServer(ctx context.Context, wgInterface *iface.WGIface) (*DefaultServer, error) {
|
||||||
|
mux := dns.NewServeMux()
|
||||||
|
listenIP := defaultIP
|
||||||
|
if runtime.GOOS != "darwin" && wgInterface != nil {
|
||||||
|
listenIP = wgInterface.GetAddress().IP.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsServer := &dns.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", listenIP, port),
|
||||||
|
Net: "udp",
|
||||||
|
Handler: mux,
|
||||||
|
UDPSize: 65535,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, stop := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
defaultServer := &DefaultServer{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: stop,
|
||||||
|
server: dnsServer,
|
||||||
|
dnsMux: mux,
|
||||||
|
dnsMuxMap: make(registrationMap),
|
||||||
|
localResolver: &localResolver{
|
||||||
|
registeredMap: make(registrationMap),
|
||||||
|
},
|
||||||
|
wgInterface: wgInterface,
|
||||||
|
runtimePort: port,
|
||||||
|
runtimeIP: listenIP,
|
||||||
|
}
|
||||||
|
|
||||||
|
hostmanager, err := newHostManager(wgInterface)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defaultServer.hostManager = hostmanager
|
||||||
|
return defaultServer, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start runs the listener in a go routine
|
||||||
|
func (s *DefaultServer) Start() {
|
||||||
|
s.runtimePort = port
|
||||||
|
udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(s.server.Addr))
|
||||||
|
probeListener, err := net.ListenUDP("udp", udpAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("using a custom port for dns server")
|
||||||
|
s.runtimePort = customPort
|
||||||
|
s.server.Addr = fmt.Sprintf("%s:%d", s.runtimeIP, customPort)
|
||||||
|
} else {
|
||||||
|
err = probeListener.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("got an error closing the probe listener, error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("starting dns on %s", s.server.Addr)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
s.setListenerStatus(true)
|
||||||
|
defer s.setListenerStatus(false)
|
||||||
|
|
||||||
|
err = s.server.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("dns server running with %d port returned an error: %v. Will not retry", s.runtimePort, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) setListenerStatus(running bool) {
|
||||||
|
s.listenerIsRunning = running
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the server
|
||||||
|
func (s *DefaultServer) Stop() {
|
||||||
|
s.mux.Lock()
|
||||||
|
defer s.mux.Unlock()
|
||||||
|
s.stop()
|
||||||
|
|
||||||
|
err := s.hostManager.restoreHostDNS()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.stopListener()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) stopListener() error {
|
||||||
|
if !s.listenerIsRunning {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := s.server.ShutdownContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stopping dns server listener returned an error: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDNSServer processes an update received from the management service
|
||||||
|
func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
log.Infof("not updating DNS server as context is closed")
|
||||||
|
return s.ctx.Err()
|
||||||
|
default:
|
||||||
|
if serial < s.updateSerial {
|
||||||
|
return fmt.Errorf("not applying dns update, error: "+
|
||||||
|
"network update is %d behind the last applied update", s.updateSerial-serial)
|
||||||
|
}
|
||||||
|
s.mux.Lock()
|
||||||
|
defer s.mux.Unlock()
|
||||||
|
|
||||||
|
// is the service should be disabled, we stop the listener
|
||||||
|
// and proceed with a regular update to clean up the handlers and records
|
||||||
|
if !update.ServiceEnable {
|
||||||
|
err := s.stopListener()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
} else if !s.listenerIsRunning {
|
||||||
|
s.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not applying dns update, error: %v", err)
|
||||||
|
}
|
||||||
|
upstreamMuxUpdates, err := s.buildUpstreamHandlerUpdate(update.NameServerGroups)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not applying dns update, error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...)
|
||||||
|
|
||||||
|
s.updateMux(muxUpdates)
|
||||||
|
s.updateLocalResolver(localRecords)
|
||||||
|
|
||||||
|
err = s.hostManager.applyDNSConfig(dnsConfigToHostDNSConfig(update, s.runtimeIP, s.runtimePort))
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.updateSerial = serial
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]muxUpdate, map[string]nbdns.SimpleRecord, error) {
|
||||||
|
var muxUpdates []muxUpdate
|
||||||
|
localRecords := make(map[string]nbdns.SimpleRecord, 0)
|
||||||
|
|
||||||
|
for _, customZone := range customZones {
|
||||||
|
|
||||||
|
if len(customZone.Records) == 0 {
|
||||||
|
return nil, nil, fmt.Errorf("received an empty list of records")
|
||||||
|
}
|
||||||
|
|
||||||
|
muxUpdates = append(muxUpdates, muxUpdate{
|
||||||
|
domain: customZone.Domain,
|
||||||
|
handler: s.localResolver,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, record := range customZone.Records {
|
||||||
|
var class uint16 = dns.ClassINET
|
||||||
|
if record.Class != nbdns.DefaultClass {
|
||||||
|
return nil, nil, fmt.Errorf("received an invalid class type: %s", record.Class)
|
||||||
|
}
|
||||||
|
key := buildRecordKey(record.Name, class, uint16(record.Type))
|
||||||
|
localRecords[key] = record
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return muxUpdates, localRecords, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.NameServerGroup) ([]muxUpdate, error) {
|
||||||
|
var muxUpdates []muxUpdate
|
||||||
|
for _, nsGroup := range nameServerGroups {
|
||||||
|
if len(nsGroup.NameServers) == 0 {
|
||||||
|
return nil, fmt.Errorf("received a nameserver group with empty nameserver list")
|
||||||
|
}
|
||||||
|
handler := &upstreamResolver{
|
||||||
|
parentCTX: s.ctx,
|
||||||
|
upstreamClient: &dns.Client{},
|
||||||
|
upstreamTimeout: defaultUpstreamTimeout,
|
||||||
|
}
|
||||||
|
for _, ns := range nsGroup.NameServers {
|
||||||
|
if ns.NSType != nbdns.UDPNameServerType {
|
||||||
|
log.Warnf("skiping nameserver %s with type %s, this peer supports only %s",
|
||||||
|
ns.IP.String(), ns.NSType.String(), nbdns.UDPNameServerType.String())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
handler.upstreamServers = append(handler.upstreamServers, getNSHostPort(ns))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(handler.upstreamServers) == 0 {
|
||||||
|
log.Errorf("received a nameserver group with an invalid nameserver list")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if nsGroup.Primary {
|
||||||
|
muxUpdates = append(muxUpdates, muxUpdate{
|
||||||
|
domain: nbdns.RootZone,
|
||||||
|
handler: handler,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(nsGroup.Domains) == 0 {
|
||||||
|
return nil, fmt.Errorf("received a non primary nameserver group with an empty domain list")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, domain := range nsGroup.Domains {
|
||||||
|
if domain == "" {
|
||||||
|
return nil, fmt.Errorf("received a nameserver group with an empty domain element")
|
||||||
|
}
|
||||||
|
muxUpdates = append(muxUpdates, muxUpdate{
|
||||||
|
domain: domain,
|
||||||
|
handler: handler,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return muxUpdates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) {
|
||||||
|
muxUpdateMap := make(registrationMap)
|
||||||
|
|
||||||
|
for _, update := range muxUpdates {
|
||||||
|
s.registerMux(update.domain, update.handler)
|
||||||
|
muxUpdateMap[update.domain] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range s.dnsMuxMap {
|
||||||
|
_, found := muxUpdateMap[key]
|
||||||
|
if !found {
|
||||||
|
s.deregisterMux(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.dnsMuxMap = muxUpdateMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) {
|
||||||
|
for key := range s.localResolver.registeredMap {
|
||||||
|
_, found := update[key]
|
||||||
|
if !found {
|
||||||
|
s.localResolver.deleteRecord(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedMap := make(registrationMap)
|
||||||
|
for key, record := range update {
|
||||||
|
err := s.localResolver.registerRecord(record)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("got an error while registering the record (%s), error: %v", record.String(), err)
|
||||||
|
}
|
||||||
|
updatedMap[key] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.localResolver.registeredMap = updatedMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNSHostPort(ns nbdns.NameServer) string {
|
||||||
|
return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) registerMux(pattern string, handler dns.Handler) {
|
||||||
|
s.dnsMux.Handle(pattern, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) deregisterMux(pattern string) {
|
||||||
|
s.dnsMux.HandleRemove(pattern)
|
||||||
|
}
|
||||||
320
client/internal/dns/server_test.go
Normal file
320
client/internal/dns/server_test.go
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var zoneRecords = []nbdns.SimpleRecord{
|
||||||
|
{
|
||||||
|
Name: "peera.netbird.cloud",
|
||||||
|
Type: 1,
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 300,
|
||||||
|
RData: "1.2.3.4",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateDNSServer(t *testing.T) {
|
||||||
|
|
||||||
|
nameServers := []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("8.8.8.8"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: 53,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("8.8.4.4"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: 53,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
initUpstreamMap registrationMap
|
||||||
|
initLocalMap registrationMap
|
||||||
|
initSerial uint64
|
||||||
|
inputSerial uint64
|
||||||
|
inputUpdate nbdns.Config
|
||||||
|
shouldFail bool
|
||||||
|
expectedUpstreamMap registrationMap
|
||||||
|
expectedLocalMap registrationMap
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Initial Config Should Succeed",
|
||||||
|
initLocalMap: make(registrationMap),
|
||||||
|
initUpstreamMap: make(registrationMap),
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{
|
||||||
|
ServiceEnable: true,
|
||||||
|
CustomZones: []nbdns.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud",
|
||||||
|
Records: zoneRecords,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
Domains: []string{"netbird.io"},
|
||||||
|
NameServers: nameServers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameServers: nameServers,
|
||||||
|
Primary: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpstreamMap: registrationMap{"netbird.io": struct{}{}, "netbird.cloud": struct{}{}, nbdns.RootZone: struct{}{}},
|
||||||
|
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "New Config Should Succeed",
|
||||||
|
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
|
||||||
|
initUpstreamMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{
|
||||||
|
ServiceEnable: true,
|
||||||
|
CustomZones: []nbdns.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud",
|
||||||
|
Records: zoneRecords,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
Domains: []string{"netbird.io"},
|
||||||
|
NameServers: nameServers,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpstreamMap: registrationMap{"netbird.io": struct{}{}, "netbird.cloud": struct{}{}},
|
||||||
|
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Smaller Config Serial Should Be Skipped",
|
||||||
|
initLocalMap: make(registrationMap),
|
||||||
|
initUpstreamMap: make(registrationMap),
|
||||||
|
initSerial: 2,
|
||||||
|
inputSerial: 1,
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty NS Group Domain Or Not Primary Element Should Fail",
|
||||||
|
initLocalMap: make(registrationMap),
|
||||||
|
initUpstreamMap: make(registrationMap),
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{
|
||||||
|
ServiceEnable: true,
|
||||||
|
CustomZones: []nbdns.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud",
|
||||||
|
Records: zoneRecords,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
NameServers: nameServers,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid NS Group Nameservers list Should Fail",
|
||||||
|
initLocalMap: make(registrationMap),
|
||||||
|
initUpstreamMap: make(registrationMap),
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{
|
||||||
|
ServiceEnable: true,
|
||||||
|
CustomZones: []nbdns.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud",
|
||||||
|
Records: zoneRecords,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
NameServers: nameServers,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid Custom Zone Records list Should Fail",
|
||||||
|
initLocalMap: make(registrationMap),
|
||||||
|
initUpstreamMap: make(registrationMap),
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{
|
||||||
|
ServiceEnable: true,
|
||||||
|
CustomZones: []nbdns.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
NameServers: nameServers,
|
||||||
|
Primary: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty Config Should Succeed and Clean Maps",
|
||||||
|
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
|
||||||
|
initUpstreamMap: registrationMap{zoneRecords[0].Name: struct{}{}},
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{ServiceEnable: true},
|
||||||
|
expectedUpstreamMap: make(registrationMap),
|
||||||
|
expectedLocalMap: make(registrationMap),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
dnsServer := getDefaultServerWithNoHostManager("127.0.0.1")
|
||||||
|
|
||||||
|
dnsServer.hostManager = newNoopHostMocker()
|
||||||
|
|
||||||
|
dnsServer.dnsMuxMap = testCase.initUpstreamMap
|
||||||
|
dnsServer.localResolver.registeredMap = testCase.initLocalMap
|
||||||
|
dnsServer.updateSerial = testCase.initSerial
|
||||||
|
// pretend we are running
|
||||||
|
dnsServer.listenerIsRunning = true
|
||||||
|
|
||||||
|
err := dnsServer.UpdateDNSServer(testCase.inputSerial, testCase.inputUpdate)
|
||||||
|
if err != nil {
|
||||||
|
if testCase.shouldFail {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("update dns server should not fail, got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dnsServer.dnsMuxMap) != len(testCase.expectedUpstreamMap) {
|
||||||
|
t.Fatalf("update upstream failed, map size is different than expected, want %d, got %d", len(testCase.expectedUpstreamMap), len(dnsServer.dnsMuxMap))
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range testCase.expectedUpstreamMap {
|
||||||
|
_, found := dnsServer.dnsMuxMap[key]
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("update upstream failed, key %s was not found in the dnsMuxMap: %#v", key, dnsServer.dnsMuxMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dnsServer.localResolver.registeredMap) != len(testCase.expectedLocalMap) {
|
||||||
|
t.Fatalf("update local failed, registered map size is different than expected, want %d, got %d", len(testCase.expectedLocalMap), len(dnsServer.localResolver.registeredMap))
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range testCase.expectedLocalMap {
|
||||||
|
_, found := dnsServer.localResolver.registeredMap[key]
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("update local failed, key %s was not found in the localResolver.registeredMap: %#v", key, dnsServer.localResolver.registeredMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSServerStartStop(t *testing.T) {
|
||||||
|
dnsServer := getDefaultServerWithNoHostManager("127.0.0.1")
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" && os.Getenv("CI") == "true" {
|
||||||
|
// todo review why this test is not working only on github actions workflows
|
||||||
|
t.Skip("skipping test in Windows CI workflows.")
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsServer.hostManager = newNoopHostMocker()
|
||||||
|
|
||||||
|
dnsServer.Start()
|
||||||
|
|
||||||
|
err := dnsServer.localResolver.registerRecord(zoneRecords[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsServer.dnsMux.Handle("netbird.cloud", dnsServer.localResolver)
|
||||||
|
|
||||||
|
resolver := &net.Resolver{
|
||||||
|
PreferGo: true,
|
||||||
|
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
d := net.Dialer{
|
||||||
|
Timeout: time.Second * 5,
|
||||||
|
}
|
||||||
|
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||||
|
conn, err := d.DialContext(ctx, network, addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
// retry test before exit, for slower systems
|
||||||
|
return d.DialContext(ctx, network, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ips, err := resolver.LookupHost(context.Background(), zoneRecords[0].Name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to connect to the server, error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log(ips)
|
||||||
|
|
||||||
|
if ips[0] != zoneRecords[0].RData {
|
||||||
|
t.Fatalf("got a different IP from the server: want %s, got %s", zoneRecords[0].RData, ips[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsServer.Stop()
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), time.Second*1)
|
||||||
|
defer cancel()
|
||||||
|
_, err = resolver.LookupHost(ctx, zoneRecords[0].Name)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("we should encounter an error when querying a stopped server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultServerWithNoHostManager(ip string) *DefaultServer {
|
||||||
|
mux := dns.NewServeMux()
|
||||||
|
listenIP := defaultIP
|
||||||
|
if ip != "" {
|
||||||
|
listenIP = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsServer := &dns.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", ip, port),
|
||||||
|
Net: "udp",
|
||||||
|
Handler: mux,
|
||||||
|
UDPSize: 65535,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, stop := context.WithCancel(context.TODO())
|
||||||
|
|
||||||
|
return &DefaultServer{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: stop,
|
||||||
|
server: dnsServer,
|
||||||
|
dnsMux: mux,
|
||||||
|
dnsMuxMap: make(registrationMap),
|
||||||
|
localResolver: &localResolver{
|
||||||
|
registeredMap: make(registrationMap),
|
||||||
|
},
|
||||||
|
runtimePort: port,
|
||||||
|
runtimeIP: listenIP,
|
||||||
|
}
|
||||||
|
}
|
||||||
185
client/internal/dns/systemd_linux.go
Normal file
185
client/internal/dns/systemd_linux.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
systemdDbusManagerInterface = "org.freedesktop.resolve1.Manager"
|
||||||
|
systemdResolvedDest = "org.freedesktop.resolve1"
|
||||||
|
systemdDbusObjectNode = "/org/freedesktop/resolve1"
|
||||||
|
systemdDbusGetLinkMethod = systemdDbusManagerInterface + ".GetLink"
|
||||||
|
systemdDbusFlushCachesMethod = systemdDbusManagerInterface + ".FlushCaches"
|
||||||
|
systemdDbusLinkInterface = "org.freedesktop.resolve1.Link"
|
||||||
|
systemdDbusRevertMethodSuffix = systemdDbusLinkInterface + ".Revert"
|
||||||
|
systemdDbusSetDNSMethodSuffix = systemdDbusLinkInterface + ".SetDNS"
|
||||||
|
systemdDbusSetDefaultRouteMethodSuffix = systemdDbusLinkInterface + ".SetDefaultRoute"
|
||||||
|
systemdDbusSetDomainsMethodSuffix = systemdDbusLinkInterface + ".SetDomains"
|
||||||
|
)
|
||||||
|
|
||||||
|
type systemdDbusConfigurator struct {
|
||||||
|
dbusLinkObject dbus.ObjectPath
|
||||||
|
routingAll bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// the types below are based on dbus specification, each field is mapped to a dbus type
|
||||||
|
// see https://dbus.freedesktop.org/doc/dbus-specification.html#basic-types for more details on dbus types
|
||||||
|
// see https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html on resolve1 input types
|
||||||
|
// systemdDbusDNSInput maps to a (iay) dbus input for SetDNS method
|
||||||
|
type systemdDbusDNSInput struct {
|
||||||
|
Family int32
|
||||||
|
Address []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// systemdDbusLinkDomainsInput maps to a (sb) dbus input for SetDomains method
|
||||||
|
type systemdDbusLinkDomainsInput struct {
|
||||||
|
Domain string
|
||||||
|
MatchOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSystemdDbusConfigurator(wgInterface *iface.WGIface) (hostManager, error) {
|
||||||
|
iface, err := net.InterfaceByName(wgInterface.GetName())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
|
||||||
|
var s string
|
||||||
|
err = obj.Call(systemdDbusGetLinkMethod, dbusDefaultFlag, iface.Index).Store(&s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("got dbus Link interface: %s from net interface %s and index %d", s, iface.Name, iface.Index)
|
||||||
|
|
||||||
|
return &systemdDbusConfigurator{
|
||||||
|
dbusLinkObject: dbus.ObjectPath(s),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemdDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
|
||||||
|
parsedIP := netip.MustParseAddr(config.serverIP).As4()
|
||||||
|
defaultLinkInput := systemdDbusDNSInput{
|
||||||
|
Family: unix.AF_INET,
|
||||||
|
Address: parsedIP[:],
|
||||||
|
}
|
||||||
|
err := s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting the interface DNS server %s:%d failed with error: %s", config.serverIP, config.serverPort, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
searchDomains []string
|
||||||
|
matchDomains []string
|
||||||
|
domainsInput []systemdDbusLinkDomainsInput
|
||||||
|
)
|
||||||
|
for _, dConf := range config.domains {
|
||||||
|
domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{
|
||||||
|
Domain: dns.Fqdn(dConf.domain),
|
||||||
|
MatchOnly: dConf.matchOnly,
|
||||||
|
})
|
||||||
|
|
||||||
|
if dConf.matchOnly {
|
||||||
|
matchDomains = append(matchDomains, dConf.domain)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
searchDomains = append(searchDomains, dConf.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.routeAll {
|
||||||
|
log.Infof("configured %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
|
||||||
|
err = s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting link as default dns router, failed with error: %s", err)
|
||||||
|
}
|
||||||
|
domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{
|
||||||
|
Domain: nbdns.RootZone,
|
||||||
|
MatchOnly: true,
|
||||||
|
})
|
||||||
|
s.routingAll = true
|
||||||
|
} else if s.routingAll {
|
||||||
|
log.Infof("removing %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("adding %d search domains and %d match domains. Search list: %s , Match list: %s", len(searchDomains), len(matchDomains), searchDomains, matchDomains)
|
||||||
|
err = s.setDomainsForInterface(domainsInput)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemdDbusConfigurator) setDomainsForInterface(domainsInput []systemdDbusLinkDomainsInput) error {
|
||||||
|
err := s.callLinkMethod(systemdDbusSetDomainsMethodSuffix, domainsInput)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting domains configuration failed with error: %s", err)
|
||||||
|
}
|
||||||
|
return s.flushCaches()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemdDbusConfigurator) restoreHostDNS() error {
|
||||||
|
log.Infof("reverting link settings and flushing cache")
|
||||||
|
if !isDbusListenerRunning(systemdResolvedDest, s.dbusLinkObject) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := s.callLinkMethod(systemdDbusRevertMethodSuffix, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to revert link configuration, got error: %s", err)
|
||||||
|
}
|
||||||
|
return s.flushCaches()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemdDbusConfigurator) flushCaches() error {
|
||||||
|
obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while attempting to retrieve the object %s, err: %s", systemdDbusObjectNode, err)
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = obj.CallWithContext(ctx, systemdDbusFlushCachesMethod, dbusDefaultFlag).Store()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while calling the FlushCaches method with context, err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemdDbusConfigurator) callLinkMethod(method string, value any) error {
|
||||||
|
obj, closeConn, err := getDbusObject(systemdResolvedDest, s.dbusLinkObject)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while attempting to retrieve the object, err: %s", err)
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if value != nil {
|
||||||
|
err = obj.CallWithContext(ctx, method, dbusDefaultFlag, value).Store()
|
||||||
|
} else {
|
||||||
|
err = obj.CallWithContext(ctx, method, dbusDefaultFlag).Store()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got error while calling command with context, err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
67
client/internal/dns/upstream.go
Normal file
67
client/internal/dns/upstream.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultUpstreamTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
type upstreamResolver struct {
|
||||||
|
parentCTX context.Context
|
||||||
|
upstreamClient *dns.Client
|
||||||
|
upstreamServers []string
|
||||||
|
upstreamTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeDNS handles a DNS request
|
||||||
|
func (u *upstreamResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
|
||||||
|
log.Tracef("received an upstream question: %#v", r.Question[0])
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-u.parentCTX.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, upstream := range u.upstreamServers {
|
||||||
|
ctx, cancel := context.WithTimeout(u.parentCTX, u.upstreamTimeout)
|
||||||
|
rm, t, err := u.upstreamClient.ExchangeContext(ctx, r, upstream)
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == context.DeadlineExceeded || isTimeout(err) {
|
||||||
|
log.Warnf("got an error while connecting to upstream %s, error: %v", upstream, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Errorf("got an error while querying the upstream %s, error: %v", upstream, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("took %s to query the upstream %s", t, upstream)
|
||||||
|
|
||||||
|
err = w.WriteMsg(rm)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("got an error while writing the upstream resolver response, error: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Errorf("all queries to the upstream nameservers failed with timeout")
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTimeout returns true if the given error is a network timeout error.
|
||||||
|
//
|
||||||
|
// Copied from k8s.io/apimachinery/pkg/util/net.IsTimeout
|
||||||
|
func isTimeout(err error) bool {
|
||||||
|
var neterr net.Error
|
||||||
|
if errors.As(err, &neterr) {
|
||||||
|
return neterr != nil && neterr.Timeout()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
110
client/internal/dns/upstream_test.go
Normal file
110
client/internal/dns/upstream_test.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpstreamResolver_ServeDNS(t *testing.T) {
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
inputMSG *dns.Msg
|
||||||
|
responseShouldBeNil bool
|
||||||
|
InputServers []string
|
||||||
|
timeout time.Duration
|
||||||
|
cancelCTX bool
|
||||||
|
expectedAnswer string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Should Resolve A Record",
|
||||||
|
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
|
||||||
|
InputServers: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
||||||
|
timeout: defaultUpstreamTimeout,
|
||||||
|
expectedAnswer: "1.1.1.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Resolve If First Upstream Times Out",
|
||||||
|
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
|
||||||
|
InputServers: []string{"8.0.0.0:53", "8.8.4.4:53"},
|
||||||
|
timeout: 2 * time.Second,
|
||||||
|
expectedAnswer: "1.1.1.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Resolve If Can't Connect To Both Servers",
|
||||||
|
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
|
||||||
|
InputServers: []string{"8.0.0.0:53", "8.0.0.1:53"},
|
||||||
|
timeout: 200 * time.Millisecond,
|
||||||
|
responseShouldBeNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Resolve If Parent Context Is Canceled",
|
||||||
|
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
|
||||||
|
InputServers: []string{"8.0.0.0:53", "8.8.4.4:53"},
|
||||||
|
cancelCTX: true,
|
||||||
|
timeout: defaultUpstreamTimeout,
|
||||||
|
responseShouldBeNil: true,
|
||||||
|
},
|
||||||
|
//{
|
||||||
|
// name: "Should Resolve CNAME Record",
|
||||||
|
// inputMSG: new(dns.Msg).SetQuestion("one.one.one.one", dns.TypeCNAME),
|
||||||
|
//},
|
||||||
|
//{
|
||||||
|
// name: "Should Not Write When Not Found A Record",
|
||||||
|
// inputMSG: new(dns.Msg).SetQuestion("not.found.com", dns.TypeA),
|
||||||
|
// responseShouldBeNil: true,
|
||||||
|
//},
|
||||||
|
}
|
||||||
|
// should resolve if first upstream times out
|
||||||
|
// should not write when both fails
|
||||||
|
// should not resolve if parent context is canceled
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
resolver := &upstreamResolver{
|
||||||
|
parentCTX: ctx,
|
||||||
|
upstreamClient: &dns.Client{},
|
||||||
|
upstreamServers: testCase.InputServers,
|
||||||
|
upstreamTimeout: testCase.timeout,
|
||||||
|
}
|
||||||
|
if testCase.cancelCTX {
|
||||||
|
cancel()
|
||||||
|
} else {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseMSG *dns.Msg
|
||||||
|
responseWriter := &mockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
responseMSG = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver.ServeDNS(responseWriter, testCase.inputMSG)
|
||||||
|
|
||||||
|
if responseMSG == nil {
|
||||||
|
if testCase.responseShouldBeNil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("should write a response message")
|
||||||
|
}
|
||||||
|
|
||||||
|
foundAnswer := false
|
||||||
|
for _, answer := range responseMSG.Answer {
|
||||||
|
if strings.Contains(answer.String(), testCase.expectedAnswer) {
|
||||||
|
foundAnswer = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundAnswer {
|
||||||
|
t.Errorf("couldn't find the required answer, %s, in the dns response", testCase.expectedAnswer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,20 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager"
|
||||||
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
||||||
|
nbstatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/internal/proxy"
|
"github.com/netbirdio/netbird/client/internal/proxy"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
@@ -45,7 +55,8 @@ type EngineConfig struct {
|
|||||||
WgPrivateKey wgtypes.Key
|
WgPrivateKey wgtypes.Key
|
||||||
|
|
||||||
// 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 map[string]struct{}
|
IFaceBlackList []string
|
||||||
|
DisableIPv6Discovery bool
|
||||||
|
|
||||||
PreSharedKey *wgtypes.Key
|
PreSharedKey *wgtypes.Key
|
||||||
|
|
||||||
@@ -54,6 +65,11 @@ type EngineConfig struct {
|
|||||||
|
|
||||||
// UDPMuxSrflxPort default value 0 - the system will pick an available port
|
// UDPMuxSrflxPort default value 0 - the system will pick an available port
|
||||||
UDPMuxSrflxPort int
|
UDPMuxSrflxPort int
|
||||||
|
|
||||||
|
// SSHKey is a private SSH key in a PEM format
|
||||||
|
SSHKey []byte
|
||||||
|
|
||||||
|
NATExternalIPs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -78,7 +94,7 @@ type Engine struct {
|
|||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
wgInterface iface.WGIface
|
wgInterface *iface.WGIface
|
||||||
|
|
||||||
udpMux ice.UDPMux
|
udpMux ice.UDPMux
|
||||||
udpMuxSrflx ice.UniversalUDPMux
|
udpMuxSrflx ice.UniversalUDPMux
|
||||||
@@ -87,6 +103,15 @@ 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
|
||||||
|
|
||||||
|
sshServerFunc func(hostKeyPEM []byte, addr string) (nbssh.Server, error)
|
||||||
|
sshServer nbssh.Server
|
||||||
|
|
||||||
|
statusRecorder *nbstatus.Status
|
||||||
|
|
||||||
|
routeManager routemanager.Manager
|
||||||
|
|
||||||
|
dnsServer dns.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
// Peer is an instance of the Connection Peer
|
// Peer is an instance of the Connection Peer
|
||||||
@@ -98,7 +123,8 @@ type Peer struct {
|
|||||||
// NewEngine creates a new Connection Engine
|
// NewEngine creates a new Connection Engine
|
||||||
func NewEngine(
|
func NewEngine(
|
||||||
ctx context.Context, cancel context.CancelFunc,
|
ctx context.Context, cancel context.CancelFunc,
|
||||||
signalClient signal.Client, mgmClient mgm.Client, config *EngineConfig,
|
signalClient signal.Client, mgmClient mgm.Client,
|
||||||
|
config *EngineConfig, statusRecorder *nbstatus.Status,
|
||||||
) *Engine {
|
) *Engine {
|
||||||
return &Engine{
|
return &Engine{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
@@ -111,6 +137,8 @@ func NewEngine(
|
|||||||
STUNs: []*ice.URL{},
|
STUNs: []*ice.URL{},
|
||||||
TURNs: []*ice.URL{},
|
TURNs: []*ice.URL{},
|
||||||
networkSerial: 0,
|
networkSerial: 0,
|
||||||
|
sshServerFunc: nbssh.DefaultSSHServer,
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +188,21 @@ func (e *Engine) Stop() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isNil(e.sshServer) {
|
||||||
|
err := e.sshServer.Stop()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed stopping the SSH server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.routeManager != nil {
|
||||||
|
e.routeManager.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.dnsServer != nil {
|
||||||
|
e.dnsServer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
log.Infof("stopped Netbird Engine")
|
log.Infof("stopped Netbird Engine")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -177,19 +220,24 @@ func (e *Engine) Start() error {
|
|||||||
myPrivateKey := e.config.WgPrivateKey
|
myPrivateKey := e.config.WgPrivateKey
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
e.wgInterface, err = iface.NewWGIface(wgIfaceName, wgAddr, iface.DefaultMTU)
|
e.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, iface.DefaultMTU)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed creating wireguard interface instance %s: [%s]", wgIfaceName, err.Error())
|
log.Errorf("failed creating wireguard interface instance %s: [%s]", wgIfaceName, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.udpMuxConn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: e.config.UDPMuxPort})
|
networkName := "udp"
|
||||||
|
if e.config.DisableIPv6Discovery {
|
||||||
|
networkName = "udp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
e.udpMuxConn, err = net.ListenUDP(networkName, &net.UDPAddr{Port: e.config.UDPMuxPort})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed listening on UDP port %d: [%s]", e.config.UDPMuxPort, err.Error())
|
log.Errorf("failed listening on UDP port %d: [%s]", e.config.UDPMuxPort, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.udpMuxConnSrflx, err = net.ListenUDP("udp4", &net.UDPAddr{Port: e.config.UDPMuxSrflxPort})
|
e.udpMuxConnSrflx, err = net.ListenUDP(networkName, &net.UDPAddr{Port: e.config.UDPMuxSrflxPort})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed listening on UDP port %d: [%s]", e.config.UDPMuxSrflxPort, err.Error())
|
log.Errorf("failed listening on UDP port %d: [%s]", e.config.UDPMuxSrflxPort, err.Error())
|
||||||
return err
|
return err
|
||||||
@@ -210,13 +258,61 @@ func (e *Engine) Start() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder)
|
||||||
|
|
||||||
|
if e.dnsServer == nil {
|
||||||
|
dnsServer, err := dns.NewDefaultServer(e.ctx, e.wgInterface)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.dnsServer = dnsServer
|
||||||
|
}
|
||||||
|
|
||||||
e.receiveSignalEvents()
|
e.receiveSignalEvents()
|
||||||
e.receiveManagementEvents()
|
e.receiveManagementEvents()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// removePeers finds and removes peers that do not exist anymore in the network map received from the Management Service
|
// modifyPeers updates peers that have been modified (e.g. IP address has been changed).
|
||||||
|
// It closes the existing connection, removes it from the peerConns map, and creates a new one.
|
||||||
|
func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
||||||
|
|
||||||
|
// first, check if peers have been modified
|
||||||
|
var modified []*mgmProto.RemotePeerConfig
|
||||||
|
for _, p := range peersUpdate {
|
||||||
|
peerPubKey := p.GetWgPubKey()
|
||||||
|
if peerConn, ok := e.peerConns[peerPubKey]; ok {
|
||||||
|
if peerConn.GetConf().ProxyConfig.AllowedIps != strings.Join(p.AllowedIps, ",") {
|
||||||
|
modified = append(modified, p)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := e.statusRecorder.UpdatePeerFQDN(peerPubKey, p.GetFqdn())
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("error updating peer's %s fqdn in the status recorder, got error: %v", peerPubKey, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// second, close all modified connections and remove them from the state map
|
||||||
|
for _, p := range modified {
|
||||||
|
err := e.removePeer(p.GetWgPubKey())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// third, add the peer connections again
|
||||||
|
for _, p := range modified {
|
||||||
|
err := e.addNewPeer(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removePeers finds and removes peers that do not exist anymore in the network map received from the Management Service.
|
||||||
|
// It also removes peers that have been modified (e.g. change of IP address). They will be added again in addPeers method.
|
||||||
func (e *Engine) removePeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
func (e *Engine) removePeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
||||||
currentPeers := make([]string, 0, len(e.peerConns))
|
currentPeers := make([]string, 0, len(e.peerConns))
|
||||||
for p := range e.peerConns {
|
for p := range e.peerConns {
|
||||||
@@ -251,9 +347,21 @@ func (e *Engine) removeAllPeers() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// removePeer closes an existing peer connection and removes a peer
|
// removePeer closes an existing peer connection, removes a peer, and clears authorized key of the SSH server
|
||||||
func (e *Engine) removePeer(peerKey string) error {
|
func (e *Engine) removePeer(peerKey string) error {
|
||||||
log.Debugf("removing peer from engine %s", peerKey)
|
log.Debugf("removing peer from engine %s", peerKey)
|
||||||
|
|
||||||
|
if !isNil(e.sshServer) {
|
||||||
|
e.sshServer.RemoveAuthorizedKey(peerKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := e.statusRecorder.RemovePeer(peerKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("received error when removing peer %s from status recorder: %v", peerKey, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
conn, exists := e.peerConns[peerKey]
|
conn, exists := e.peerConns[peerKey]
|
||||||
if exists {
|
if exists {
|
||||||
delete(e.peerConns, peerKey)
|
delete(e.peerConns, peerKey)
|
||||||
@@ -316,15 +424,14 @@ func signalCandidate(candidate ice.Candidate, myKey wgtypes.Key, remoteKey wgtyp
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed signaling candidate to the remote peer %s %s", remoteKey.String(), err)
|
|
||||||
// todo ??
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalAuth(uFrag string, pwd string, myKey wgtypes.Key, remoteKey wgtypes.Key, s signal.Client, isAnswer bool) error {
|
// 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
|
var t sProto.Body_Type
|
||||||
if isAnswer {
|
if isAnswer {
|
||||||
t = sProto.Body_ANSWER
|
t = sProto.Body_ANSWER
|
||||||
@@ -332,9 +439,9 @@ func signalAuth(uFrag string, pwd string, myKey wgtypes.Key, remoteKey wgtypes.K
|
|||||||
t = sProto.Body_OFFER
|
t = sProto.Body_OFFER
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, err := signal.MarshalCredential(myKey, remoteKey, &signal.Credential{
|
msg, err := signal.MarshalCredential(myKey, offerAnswer.WgListenPort, remoteKey, &signal.Credential{
|
||||||
UFrag: uFrag,
|
UFrag: offerAnswer.IceCredentials.UFrag,
|
||||||
Pwd: pwd,
|
Pwd: offerAnswer.IceCredentials.Pwd,
|
||||||
}, t)
|
}, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -376,6 +483,82 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isNil(server nbssh.Server) bool {
|
||||||
|
return server == nil || reflect.ValueOf(server).IsNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error {
|
||||||
|
if sshConf.GetSshEnabled() {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
log.Warnf("running SSH server on Windows is not supported")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// start SSH server if it wasn't running
|
||||||
|
if isNil(e.sshServer) {
|
||||||
|
//nil sshServer means it has not yet been started
|
||||||
|
var err error
|
||||||
|
e.sshServer, err = e.sshServerFunc(e.config.SSHKey,
|
||||||
|
fmt.Sprintf("%s:%d", e.wgInterface.Address.IP.String(), nbssh.DefaultSSHPort))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
// blocking
|
||||||
|
err = e.sshServer.Start()
|
||||||
|
if err != nil {
|
||||||
|
// will throw error when we stop it even if it is a graceful stop
|
||||||
|
log.Debugf("stopped SSH server with error %v", err)
|
||||||
|
}
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
e.sshServer = nil
|
||||||
|
log.Infof("stopped SSH server")
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
log.Debugf("SSH server is already running")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Disable SSH server request, so stop it if it was running
|
||||||
|
if !isNil(e.sshServer) {
|
||||||
|
err := e.sshServer.Stop()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to stop SSH server %v", err)
|
||||||
|
}
|
||||||
|
e.sshServer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
|
||||||
|
if e.wgInterface.Address.String() != conf.Address {
|
||||||
|
oldAddr := e.wgInterface.Address.String()
|
||||||
|
log.Debugf("updating peer address from %s to %s", oldAddr, conf.Address)
|
||||||
|
err := e.wgInterface.UpdateAddr(conf.Address)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.config.WgAddr = conf.Address
|
||||||
|
log.Infof("updated peer address from %s to %s", oldAddr, conf.Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.GetSshConfig() != nil {
|
||||||
|
err := e.updateSSH(conf.GetSshConfig())
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed handling SSH server setup %v", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.statusRecorder.UpdateLocalPeerState(nbstatus.LocalPeerState{
|
||||||
|
IP: e.config.WgAddr,
|
||||||
|
PubKey: e.config.WgPrivateKey.PublicKey().String(),
|
||||||
|
KernelInterface: iface.WireguardModuleIsLoaded(),
|
||||||
|
FQDN: conf.GetFqdn(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// receiveManagementEvents connects to the Management Service event stream to receive updates from the management service
|
// receiveManagementEvents connects to the Management Service event stream to receive updates from the management service
|
||||||
// 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() {
|
||||||
@@ -434,6 +617,15 @@ func (e *Engine) updateTURNs(turns []*mgmProto.ProtectedHostConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
||||||
|
|
||||||
|
// intentionally leave it before checking serial because for now it can happen that peer IP changed but serial didn't
|
||||||
|
if networkMap.GetPeerConfig() != nil {
|
||||||
|
err := e.updateConfig(networkMap.GetPeerConfig())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
serial := networkMap.GetSerial()
|
serial := networkMap.GetSerial()
|
||||||
if e.networkSerial > serial {
|
if e.networkSerial > serial {
|
||||||
log.Debugf("received outdated NetworkMap with serial %d, ignoring", serial)
|
log.Debugf("received outdated NetworkMap with serial %d, ignoring", serial)
|
||||||
@@ -454,21 +646,125 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = e.modifyPeers(networkMap.GetRemotePeers())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = e.addNewPeers(networkMap.GetRemotePeers())
|
err = e.addNewPeers(networkMap.GetRemotePeers())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update SSHServer by adding remote peer SSH keys
|
||||||
|
if !isNil(e.sshServer) {
|
||||||
|
for _, config := range networkMap.GetRemotePeers() {
|
||||||
|
if config.GetSshConfig() != nil && config.GetSshConfig().GetSshPubKey() != nil {
|
||||||
|
err := e.sshServer.AddAuthorizedKey(config.WgPubKey, string(config.GetSshConfig().GetSshPubKey()))
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed adding authroized key to SSH DefaultServer %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
if protoDNSConfig == nil {
|
||||||
|
protoDNSConfig = &mgmProto.DNSConfig{}
|
||||||
|
}
|
||||||
|
err = e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to update dns server, err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.networkSerial = serial
|
e.networkSerial = serial
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// addNewPeers finds and adds peers that were not know before but arrived from the Management service with the update
|
func toRoutes(protoRoutes []*mgmProto.Route) []*route.Route {
|
||||||
|
routes := make([]*route.Route, 0)
|
||||||
|
for _, protoRoute := range protoRoutes {
|
||||||
|
_, prefix, _ := route.ParseNetwork(protoRoute.Network)
|
||||||
|
convertedRoute := &route.Route{
|
||||||
|
ID: protoRoute.ID,
|
||||||
|
Network: prefix,
|
||||||
|
NetID: protoRoute.NetID,
|
||||||
|
NetworkType: route.NetworkType(protoRoute.NetworkType),
|
||||||
|
Peer: protoRoute.Peer,
|
||||||
|
Metric: int(protoRoute.Metric),
|
||||||
|
Masquerade: protoRoute.Masquerade,
|
||||||
|
}
|
||||||
|
routes = append(routes, convertedRoute)
|
||||||
|
}
|
||||||
|
return routes
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig) nbdns.Config {
|
||||||
|
dnsUpdate := nbdns.Config{
|
||||||
|
ServiceEnable: protoDNSConfig.GetServiceEnable(),
|
||||||
|
CustomZones: make([]nbdns.CustomZone, 0),
|
||||||
|
NameServerGroups: make([]*nbdns.NameServerGroup, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, zone := range protoDNSConfig.GetCustomZones() {
|
||||||
|
dnsZone := nbdns.CustomZone{
|
||||||
|
Domain: zone.GetDomain(),
|
||||||
|
}
|
||||||
|
for _, record := range zone.Records {
|
||||||
|
dnsRecord := nbdns.SimpleRecord{
|
||||||
|
Name: record.GetName(),
|
||||||
|
Type: int(record.GetType()),
|
||||||
|
Class: record.GetClass(),
|
||||||
|
TTL: int(record.GetTTL()),
|
||||||
|
RData: record.GetRData(),
|
||||||
|
}
|
||||||
|
dnsZone.Records = append(dnsZone.Records, dnsRecord)
|
||||||
|
}
|
||||||
|
dnsUpdate.CustomZones = append(dnsUpdate.CustomZones, dnsZone)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, nsGroup := range protoDNSConfig.GetNameServerGroups() {
|
||||||
|
dnsNSGroup := &nbdns.NameServerGroup{
|
||||||
|
Primary: nsGroup.GetPrimary(),
|
||||||
|
Domains: nsGroup.GetDomains(),
|
||||||
|
}
|
||||||
|
for _, ns := range nsGroup.GetNameServers() {
|
||||||
|
dnsNS := nbdns.NameServer{
|
||||||
|
IP: netip.MustParseAddr(ns.GetIP()),
|
||||||
|
NSType: nbdns.NameServerType(ns.GetNSType()),
|
||||||
|
Port: int(ns.GetPort()),
|
||||||
|
}
|
||||||
|
dnsNSGroup.NameServers = append(dnsNSGroup.NameServers, dnsNS)
|
||||||
|
}
|
||||||
|
dnsUpdate.NameServerGroups = append(dnsUpdate.NameServerGroups, dnsNSGroup)
|
||||||
|
}
|
||||||
|
return dnsUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNewPeers adds peers that were not know before but arrived from the Management service with the update
|
||||||
func (e *Engine) addNewPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
func (e *Engine) addNewPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
||||||
for _, p := range peersUpdate {
|
for _, p := range peersUpdate {
|
||||||
peerKey := p.GetWgPubKey()
|
err := e.addNewPeer(p)
|
||||||
peerIPs := p.GetAllowedIps()
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNewPeer add peer if connection doesn't exist
|
||||||
|
func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
|
||||||
|
peerKey := peerConfig.GetWgPubKey()
|
||||||
|
peerIPs := peerConfig.GetAllowedIps()
|
||||||
if _, ok := e.peerConns[peerKey]; !ok {
|
if _, ok := e.peerConns[peerKey]; !ok {
|
||||||
conn, err := e.createPeerConn(peerKey, strings.Join(peerIPs, ","))
|
conn, err := e.createPeerConn(peerKey, strings.Join(peerIPs, ","))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -476,14 +772,21 @@ func (e *Engine) addNewPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
|||||||
}
|
}
|
||||||
e.peerConns[peerKey] = conn
|
e.peerConns[peerKey] = conn
|
||||||
|
|
||||||
go e.connWorker(conn, peerKey)
|
err = e.statusRecorder.AddPeer(peerKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go e.connWorker(conn, peerKey)
|
||||||
|
}
|
||||||
|
err := e.statusRecorder.UpdatePeerFQDN(peerKey, peerConfig.Fqdn)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("error updating peer's %s fqdn in the status recorder, got error: %v", peerKey, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Engine) connWorker(conn *peer.Conn, peerKey string) {
|
func (e *Engine) connWorker(conn *peer.Conn, peerKey string) {
|
||||||
for {
|
for {
|
||||||
|
|
||||||
// randomize starting time a bit
|
// randomize starting time a bit
|
||||||
@@ -502,9 +805,22 @@ func (e Engine) connWorker(conn *peer.Conn, peerKey string) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we might have received new STUN and TURN servers meanwhile, so update them
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
conf := conn.GetConf()
|
||||||
|
conf.StunTurn = append(e.STUNs, e.TURNs...)
|
||||||
|
conn.UpdateConf(conf)
|
||||||
|
e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
err := conn.Open()
|
err := conn.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("connection to peer %s failed: %v", peerKey, err)
|
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:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,15 +833,11 @@ func (e Engine) peerExists(peerKey string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
var stunTurn []*ice.URL
|
var stunTurn []*ice.URL
|
||||||
stunTurn = append(stunTurn, e.STUNs...)
|
stunTurn = append(stunTurn, e.STUNs...)
|
||||||
stunTurn = append(stunTurn, e.TURNs...)
|
stunTurn = append(stunTurn, e.TURNs...)
|
||||||
|
|
||||||
interfaceBlacklist := make([]string, 0, len(e.config.IFaceBlackList))
|
|
||||||
for k := range e.config.IFaceBlackList {
|
|
||||||
interfaceBlacklist = append(interfaceBlacklist, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyConfig := proxy.Config{
|
proxyConfig := proxy.Config{
|
||||||
RemoteKey: pubKey,
|
RemoteKey: pubKey,
|
||||||
WgListenAddr: fmt.Sprintf("127.0.0.1:%d", e.config.WgPort),
|
WgListenAddr: fmt.Sprintf("127.0.0.1:%d", e.config.WgPort),
|
||||||
@@ -540,14 +852,17 @@ func (e Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, er
|
|||||||
Key: pubKey,
|
Key: pubKey,
|
||||||
LocalKey: e.config.WgPrivateKey.PublicKey().String(),
|
LocalKey: e.config.WgPrivateKey.PublicKey().String(),
|
||||||
StunTurn: stunTurn,
|
StunTurn: stunTurn,
|
||||||
InterfaceBlackList: interfaceBlacklist,
|
InterfaceBlackList: e.config.IFaceBlackList,
|
||||||
|
DisableIPv6Discovery: e.config.DisableIPv6Discovery,
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
UDPMux: e.udpMux,
|
UDPMux: e.udpMux,
|
||||||
UDPMuxSrflx: e.udpMuxSrflx,
|
UDPMuxSrflx: e.udpMuxSrflx,
|
||||||
ProxyConfig: proxyConfig,
|
ProxyConfig: proxyConfig,
|
||||||
|
LocalWgPort: e.config.WgPort,
|
||||||
|
NATExternalIPs: e.parseNATExternalIPMappings(),
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConn, err := peer.NewConn(config)
|
peerConn, err := peer.NewConn(config, e.statusRecorder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -557,16 +872,16 @@ func (e Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, er
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
signalOffer := func(uFrag string, pwd string) error {
|
signalOffer := func(offerAnswer peer.OfferAnswer) error {
|
||||||
return signalAuth(uFrag, pwd, e.config.WgPrivateKey, wgPubKey, e.signal, false)
|
return SignalOfferAnswer(offerAnswer, e.config.WgPrivateKey, wgPubKey, e.signal, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
signalCandidate := func(candidate ice.Candidate) error {
|
signalCandidate := func(candidate ice.Candidate) error {
|
||||||
return signalCandidate(candidate, e.config.WgPrivateKey, wgPubKey, e.signal)
|
return signalCandidate(candidate, e.config.WgPrivateKey, wgPubKey, e.signal)
|
||||||
}
|
}
|
||||||
|
|
||||||
signalAnswer := func(uFrag string, pwd string) error {
|
signalAnswer := func(offerAnswer peer.OfferAnswer) error {
|
||||||
return signalAuth(uFrag, pwd, e.config.WgPrivateKey, wgPubKey, e.signal, true)
|
return SignalOfferAnswer(offerAnswer, e.config.WgPrivateKey, wgPubKey, e.signal, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConn.SetSignalCandidate(signalCandidate)
|
peerConn.SetSignalCandidate(signalCandidate)
|
||||||
@@ -595,18 +910,26 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn.OnRemoteOffer(peer.IceCredentials{
|
conn.OnRemoteOffer(peer.OfferAnswer{
|
||||||
|
IceCredentials: peer.IceCredentials{
|
||||||
UFrag: remoteCred.UFrag,
|
UFrag: remoteCred.UFrag,
|
||||||
Pwd: remoteCred.Pwd,
|
Pwd: remoteCred.Pwd,
|
||||||
|
},
|
||||||
|
WgListenPort: int(msg.GetBody().GetWgListenPort()),
|
||||||
|
Version: msg.GetBody().GetNetBirdVersion(),
|
||||||
})
|
})
|
||||||
case sProto.Body_ANSWER:
|
case sProto.Body_ANSWER:
|
||||||
remoteCred, err := signal.UnMarshalCredential(msg)
|
remoteCred, err := signal.UnMarshalCredential(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn.OnRemoteAnswer(peer.IceCredentials{
|
conn.OnRemoteAnswer(peer.OfferAnswer{
|
||||||
|
IceCredentials: peer.IceCredentials{
|
||||||
UFrag: remoteCred.UFrag,
|
UFrag: remoteCred.UFrag,
|
||||||
Pwd: remoteCred.Pwd,
|
Pwd: remoteCred.Pwd,
|
||||||
|
},
|
||||||
|
WgListenPort: int(msg.GetBody().GetWgListenPort()),
|
||||||
|
Version: msg.GetBody().GetNetBirdVersion(),
|
||||||
})
|
})
|
||||||
case sProto.Body_CANDIDATE:
|
case sProto.Body_CANDIDATE:
|
||||||
candidate, err := ice.UnmarshalCandidate(msg.GetBody().Payload)
|
candidate, err := ice.UnmarshalCandidate(msg.GetBody().Payload)
|
||||||
@@ -630,3 +953,77 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
|
|
||||||
e.signal.WaitStreamConnected()
|
e.signal.WaitStreamConnected()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Engine) parseNATExternalIPMappings() []string {
|
||||||
|
var mappedIPs []string
|
||||||
|
var ignoredIFaces = make(map[string]interface{})
|
||||||
|
for _, iFace := range e.config.IFaceBlackList {
|
||||||
|
ignoredIFaces[iFace] = nil
|
||||||
|
}
|
||||||
|
for _, mapping := range e.config.NATExternalIPs {
|
||||||
|
var external, internal string
|
||||||
|
var externalIP, internalIP net.IP
|
||||||
|
var err error
|
||||||
|
split := strings.Split(mapping, "/")
|
||||||
|
if len(split) > 2 {
|
||||||
|
log.Warnf("ignoring invalid external mapping '%s', too many delimiters", mapping)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if len(split) > 1 {
|
||||||
|
internal = split[1]
|
||||||
|
internalIP = net.ParseIP(internal)
|
||||||
|
if internalIP == nil {
|
||||||
|
// not a properly formatted IP address, maybe it's interface name?
|
||||||
|
if _, present := ignoredIFaces[internal]; present {
|
||||||
|
log.Warnf("internal interface '%s' in blacklist, ignoring external mapping '%s'", internal, mapping)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
internalIP, err = findIPFromInterfaceName(internal)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("error finding interface IP for interface '%s', ignoring external mapping '%s': %v", internal, mapping, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
external = split[0]
|
||||||
|
externalIP = net.ParseIP(external)
|
||||||
|
if externalIP == nil {
|
||||||
|
log.Warnf("invalid external IP, ignoring external IP mapping '%s'", mapping)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if externalIP != nil {
|
||||||
|
mappedIP := externalIP.String()
|
||||||
|
if internalIP != nil {
|
||||||
|
mappedIP = mappedIP + "/" + internalIP.String()
|
||||||
|
}
|
||||||
|
mappedIPs = append(mappedIPs, mappedIP)
|
||||||
|
log.Infof("parsed external IP mapping of '%s' as '%s'", mapping, mappedIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(mappedIPs) != len(e.config.NATExternalIPs) {
|
||||||
|
log.Warnf("one or more external IP mappings failed to parse, ignoring all mappings")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return mappedIPs
|
||||||
|
}
|
||||||
|
|
||||||
|
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
|
||||||
|
iface, err := net.InterfaceByName(ifaceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return findIPFromInterface(iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findIPFromInterface(iface *net.Interface) (net.IP, error) {
|
||||||
|
ifaceAddrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, addr := range ifaceAddrs {
|
||||||
|
if ipv4Addr := addr.(*net.IPNet).IP.To4(); ipv4Addr != nil {
|
||||||
|
return ipv4Addr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("interface %s don't have an ipv4 address", iface.Name)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,20 @@ package internal
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager"
|
||||||
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
|
nbstatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -39,6 +49,140 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestEngine_SSH(t *testing.T) {
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("skipping TestEngine_SSH on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
||||||
|
WgIfaceName: "utun101",
|
||||||
|
WgAddr: "100.64.0.1/24",
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: 33100,
|
||||||
|
}, nbstatus.NewRecorder())
|
||||||
|
|
||||||
|
var sshKeysAdded []string
|
||||||
|
var sshPeersRemoved []string
|
||||||
|
|
||||||
|
sshCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
engine.sshServerFunc = func(hostKeyPEM []byte, addr string) (ssh.Server, error) {
|
||||||
|
return &ssh.MockServer{
|
||||||
|
Ctx: sshCtx,
|
||||||
|
StopFunc: func() error {
|
||||||
|
cancel()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
StartFunc: func() error {
|
||||||
|
<-ctx.Done()
|
||||||
|
return ctx.Err()
|
||||||
|
},
|
||||||
|
AddAuthorizedKeyFunc: func(peer, newKey string) error {
|
||||||
|
sshKeysAdded = append(sshKeysAdded, newKey)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
RemoveAuthorizedKeyFunc: func(peer string) {
|
||||||
|
sshPeersRemoved = append(sshPeersRemoved, peer)
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
err = engine.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := engine.Stop()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
peerWithSSH := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "MNHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.21/24"},
|
||||||
|
SshConfig: &mgmtProto.SSHConfig{
|
||||||
|
SshPubKey: []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFATYCqaQw/9id1Qkq3n16JYhDhXraI6Pc1fgB8ynEfQ"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSH server is not enabled so SSH config of a remote peer should be ignored
|
||||||
|
networkMap := &mgmtProto.NetworkMap{
|
||||||
|
Serial: 6,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.updateNetworkMap(networkMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Nil(t, engine.sshServer)
|
||||||
|
|
||||||
|
// SSH server is enabled, therefore SSH config should be applied
|
||||||
|
networkMap = &mgmtProto.NetworkMap{
|
||||||
|
Serial: 7,
|
||||||
|
PeerConfig: &mgmtProto.PeerConfig{Address: "100.64.0.1/24",
|
||||||
|
SshConfig: &mgmtProto.SSHConfig{SshEnabled: true}},
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.updateNetworkMap(networkMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
assert.NotNil(t, engine.sshServer)
|
||||||
|
assert.Contains(t, sshKeysAdded, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFATYCqaQw/9id1Qkq3n16JYhDhXraI6Pc1fgB8ynEfQ")
|
||||||
|
|
||||||
|
// now remove peer
|
||||||
|
networkMap = &mgmtProto.NetworkMap{
|
||||||
|
Serial: 8,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.updateNetworkMap(networkMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//time.Sleep(250 * time.Millisecond)
|
||||||
|
assert.NotNil(t, engine.sshServer)
|
||||||
|
assert.Contains(t, sshPeersRemoved, "MNHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=")
|
||||||
|
|
||||||
|
// now disable SSH server
|
||||||
|
networkMap = &mgmtProto.NetworkMap{
|
||||||
|
Serial: 9,
|
||||||
|
PeerConfig: &mgmtProto.PeerConfig{Address: "100.64.0.1/24",
|
||||||
|
SshConfig: &mgmtProto.SSHConfig{SshEnabled: false}},
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.updateNetworkMap(networkMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Nil(t, engine.sshServer)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestEngine_UpdateNetworkMap(t *testing.T) {
|
func TestEngine_UpdateNetworkMap(t *testing.T) {
|
||||||
// test setup
|
// test setup
|
||||||
key, err := wgtypes.GeneratePrivateKey()
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
@@ -51,18 +195,23 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
||||||
WgIfaceName: "utun100",
|
WgIfaceName: "utun102",
|
||||||
WgAddr: "100.64.0.1/24",
|
WgAddr: "100.64.0.1/24",
|
||||||
WgPrivateKey: key,
|
WgPrivateKey: key,
|
||||||
WgPort: 33100,
|
WgPort: 33100,
|
||||||
})
|
}, nbstatus.NewRecorder())
|
||||||
|
engine.wgInterface, err = iface.NewWGIFace("utun102", "100.64.0.1/24", iface.DefaultMTU)
|
||||||
|
engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), engine.wgInterface, engine.statusRecorder)
|
||||||
|
engine.dnsServer = &dns.MockServer{
|
||||||
|
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
|
||||||
|
}
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
networkMap *mgmtProto.NetworkMap
|
networkMap *mgmtProto.NetworkMap
|
||||||
|
|
||||||
expectedLen int
|
expectedLen int
|
||||||
expectedPeers []string
|
expectedPeers []*mgmtProto.RemotePeerConfig
|
||||||
expectedSerial uint64
|
expectedSerial uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +230,11 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
AllowedIps: []string{"100.64.0.12/24"},
|
AllowedIps: []string{"100.64.0.12/24"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modifiedPeer3 := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "GGHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.20/24"},
|
||||||
|
}
|
||||||
|
|
||||||
case1 := testCase{
|
case1 := testCase{
|
||||||
name: "input with a new peer to add",
|
name: "input with a new peer to add",
|
||||||
networkMap: &mgmtProto.NetworkMap{
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
@@ -92,7 +246,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
RemotePeersIsEmpty: false,
|
RemotePeersIsEmpty: false,
|
||||||
},
|
},
|
||||||
expectedLen: 1,
|
expectedLen: 1,
|
||||||
expectedPeers: []string{peer1.GetWgPubKey()},
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer1},
|
||||||
expectedSerial: 1,
|
expectedSerial: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +262,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
RemotePeersIsEmpty: false,
|
RemotePeersIsEmpty: false,
|
||||||
},
|
},
|
||||||
expectedLen: 2,
|
expectedLen: 2,
|
||||||
expectedPeers: []string{peer1.GetWgPubKey(), peer2.GetWgPubKey()},
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer1, peer2},
|
||||||
expectedSerial: 2,
|
expectedSerial: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +277,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
RemotePeersIsEmpty: false,
|
RemotePeersIsEmpty: false,
|
||||||
},
|
},
|
||||||
expectedLen: 2,
|
expectedLen: 2,
|
||||||
expectedPeers: []string{peer1.GetWgPubKey(), peer2.GetWgPubKey()},
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer1, peer2},
|
||||||
expectedSerial: 2,
|
expectedSerial: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,11 +292,26 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
RemotePeersIsEmpty: false,
|
RemotePeersIsEmpty: false,
|
||||||
},
|
},
|
||||||
expectedLen: 2,
|
expectedLen: 2,
|
||||||
expectedPeers: []string{peer2.GetWgPubKey(), peer3.GetWgPubKey()},
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer2, peer3},
|
||||||
expectedSerial: 4,
|
expectedSerial: 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
case5 := testCase{
|
case5 := testCase{
|
||||||
|
name: "input with one peer to modify",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 4,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
||||||
|
modifiedPeer3, peer2,
|
||||||
|
},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
},
|
||||||
|
expectedLen: 2,
|
||||||
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer2, modifiedPeer3},
|
||||||
|
expectedSerial: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
case6 := testCase{
|
||||||
name: "input with all peers to remove",
|
name: "input with all peers to remove",
|
||||||
networkMap: &mgmtProto.NetworkMap{
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
Serial: 5,
|
Serial: 5,
|
||||||
@@ -155,7 +324,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
expectedSerial: 5,
|
expectedSerial: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range []testCase{case1, case2, case3, case4, case5} {
|
for _, c := range []testCase{case1, case2, case3, case4, case5, case6} {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
err = engine.updateNetworkMap(c.networkMap)
|
err = engine.updateNetworkMap(c.networkMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -172,9 +341,15 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range c.expectedPeers {
|
for _, p := range c.expectedPeers {
|
||||||
if _, ok := engine.peerConns[p]; !ok {
|
conn, ok := engine.peerConns[p.GetWgPubKey()]
|
||||||
|
if !ok {
|
||||||
t.Errorf("expecting Engine.peerConns to contain peer %s", p)
|
t.Errorf("expecting Engine.peerConns to contain peer %s", p)
|
||||||
}
|
}
|
||||||
|
expectedAllowedIPs := strings.Join(p.AllowedIps, ",")
|
||||||
|
if conn.GetConf().ProxyConfig.AllowedIps != expectedAllowedIPs {
|
||||||
|
t.Errorf("expecting peer %s to have AllowedIPs= %s, got %s", p.GetWgPubKey(),
|
||||||
|
expectedAllowedIPs, conn.GetConf().ProxyConfig.AllowedIps)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -204,11 +379,11 @@ func TestEngine_Sync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{SyncFunc: syncFunc}, &EngineConfig{
|
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{SyncFunc: syncFunc}, &EngineConfig{
|
||||||
WgIfaceName: "utun100",
|
WgIfaceName: "utun103",
|
||||||
WgAddr: "100.64.0.1/24",
|
WgAddr: "100.64.0.1/24",
|
||||||
WgPrivateKey: key,
|
WgPrivateKey: key,
|
||||||
WgPort: 33100,
|
WgPort: 33100,
|
||||||
})
|
}, nbstatus.NewRecorder())
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := engine.Stop()
|
err := engine.Stop()
|
||||||
@@ -260,6 +435,320 @@ func TestEngine_Sync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
inputErr error
|
||||||
|
networkMap *mgmtProto.NetworkMap
|
||||||
|
expectedLen int
|
||||||
|
expectedRoutes []*route.Route
|
||||||
|
expectedSerial uint64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Routes Config Should Be Passed To Manager",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 1,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
Routes: []*mgmtProto.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
Network: "192.168.0.0/24",
|
||||||
|
NetID: "n1",
|
||||||
|
Peer: "p1",
|
||||||
|
NetworkType: 1,
|
||||||
|
Masquerade: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
Network: "192.168.1.0/24",
|
||||||
|
NetID: "n2",
|
||||||
|
Peer: "p1",
|
||||||
|
NetworkType: 1,
|
||||||
|
Masquerade: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedLen: 2,
|
||||||
|
expectedRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
Network: netip.MustParsePrefix("192.168.0.0/24"),
|
||||||
|
NetID: "n1",
|
||||||
|
Peer: "p1",
|
||||||
|
NetworkType: 1,
|
||||||
|
Masquerade: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
Network: netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
|
NetID: "n2",
|
||||||
|
Peer: "p1",
|
||||||
|
NetworkType: 1,
|
||||||
|
Masquerade: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSerial: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty Routes Config Should Be Passed",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 1,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
Routes: nil,
|
||||||
|
},
|
||||||
|
expectedLen: 0,
|
||||||
|
expectedRoutes: []*route.Route{},
|
||||||
|
expectedSerial: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error Shouldn't Break Engine",
|
||||||
|
inputErr: fmt.Errorf("mocking error"),
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 1,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
Routes: nil,
|
||||||
|
},
|
||||||
|
expectedLen: 0,
|
||||||
|
expectedRoutes: []*route.Route{},
|
||||||
|
expectedSerial: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
// test setup
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
||||||
|
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
|
||||||
|
|
||||||
|
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
||||||
|
WgIfaceName: wgIfaceName,
|
||||||
|
WgAddr: wgAddr,
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: 33100,
|
||||||
|
}, nbstatus.NewRecorder())
|
||||||
|
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, iface.DefaultMTU)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
input := struct {
|
||||||
|
inputSerial uint64
|
||||||
|
inputRoutes []*route.Route
|
||||||
|
}{}
|
||||||
|
|
||||||
|
mockRouteManager := &routemanager.MockManager{
|
||||||
|
UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) error {
|
||||||
|
input.inputSerial = updateSerial
|
||||||
|
input.inputRoutes = newRoutes
|
||||||
|
return testCase.inputErr
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.routeManager = mockRouteManager
|
||||||
|
engine.dnsServer = &dns.MockServer{}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
exitErr := engine.Stop()
|
||||||
|
if exitErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = engine.updateNetworkMap(testCase.networkMap)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
||||||
|
assert.Len(t, input.inputRoutes, testCase.expectedLen, "routes len should match")
|
||||||
|
assert.Equal(t, testCase.expectedRoutes, input.inputRoutes, "routes should match")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
inputErr error
|
||||||
|
networkMap *mgmtProto.NetworkMap
|
||||||
|
expectedZonesLen int
|
||||||
|
expectedZones []nbdns.CustomZone
|
||||||
|
expectedNSGroupsLen int
|
||||||
|
expectedNSGroups []*nbdns.NameServerGroup
|
||||||
|
expectedSerial uint64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "DNS Config Should Be Passed To DNS Server",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 1,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
Routes: nil,
|
||||||
|
DNSConfig: &mgmtProto.DNSConfig{
|
||||||
|
ServiceEnable: true,
|
||||||
|
CustomZones: []*mgmtProto.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud.",
|
||||||
|
Records: []*mgmtProto.SimpleRecord{
|
||||||
|
{
|
||||||
|
Name: "peer-a.netbird.cloud.",
|
||||||
|
Type: 1,
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 300,
|
||||||
|
RData: "100.64.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NameServerGroups: []*mgmtProto.NameServerGroup{
|
||||||
|
{
|
||||||
|
Primary: true,
|
||||||
|
NameServers: []*mgmtProto.NameServer{
|
||||||
|
{
|
||||||
|
IP: "8.8.8.8",
|
||||||
|
NSType: 1,
|
||||||
|
Port: 53,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedZonesLen: 1,
|
||||||
|
expectedZones: []nbdns.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud.",
|
||||||
|
Records: []nbdns.SimpleRecord{
|
||||||
|
{
|
||||||
|
Name: "peer-a.netbird.cloud.",
|
||||||
|
Type: 1,
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 300,
|
||||||
|
RData: "100.64.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedNSGroupsLen: 1,
|
||||||
|
expectedNSGroups: []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
Primary: true,
|
||||||
|
NameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("8.8.8.8"),
|
||||||
|
NSType: 1,
|
||||||
|
Port: 53,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSerial: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty DNS Config Should Be OK",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 1,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
Routes: nil,
|
||||||
|
DNSConfig: nil,
|
||||||
|
},
|
||||||
|
expectedZonesLen: 0,
|
||||||
|
expectedZones: []nbdns.CustomZone{},
|
||||||
|
expectedNSGroupsLen: 0,
|
||||||
|
expectedNSGroups: []*nbdns.NameServerGroup{},
|
||||||
|
expectedSerial: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error Shouldn't Break Engine",
|
||||||
|
inputErr: fmt.Errorf("mocking error"),
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 1,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
Routes: nil,
|
||||||
|
},
|
||||||
|
expectedZonesLen: 0,
|
||||||
|
expectedZones: []nbdns.CustomZone{},
|
||||||
|
expectedNSGroupsLen: 0,
|
||||||
|
expectedNSGroups: []*nbdns.NameServerGroup{},
|
||||||
|
expectedSerial: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
// test setup
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
||||||
|
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
|
||||||
|
|
||||||
|
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
||||||
|
WgIfaceName: wgIfaceName,
|
||||||
|
WgAddr: wgAddr,
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: 33100,
|
||||||
|
}, nbstatus.NewRecorder())
|
||||||
|
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, iface.DefaultMTU)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
mockRouteManager := &routemanager.MockManager{
|
||||||
|
UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.routeManager = mockRouteManager
|
||||||
|
|
||||||
|
input := struct {
|
||||||
|
inputSerial uint64
|
||||||
|
inputNSGroups []*nbdns.NameServerGroup
|
||||||
|
inputZones []nbdns.CustomZone
|
||||||
|
}{}
|
||||||
|
|
||||||
|
mockDNSServer := &dns.MockServer{
|
||||||
|
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error {
|
||||||
|
input.inputSerial = serial
|
||||||
|
input.inputZones = update.CustomZones
|
||||||
|
input.inputNSGroups = update.NameServerGroups
|
||||||
|
return testCase.inputErr
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.dnsServer = mockDNSServer
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
exitErr := engine.Stop()
|
||||||
|
if exitErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = engine.updateNetworkMap(testCase.networkMap)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
||||||
|
assert.Len(t, input.inputNSGroups, testCase.expectedZonesLen, "zones len should match")
|
||||||
|
assert.Equal(t, testCase.expectedZones, input.inputZones, "custom zones should match")
|
||||||
|
assert.Len(t, input.inputNSGroups, testCase.expectedNSGroupsLen, "ns groups len should match")
|
||||||
|
assert.Equal(t, testCase.expectedNSGroups, input.inputNSGroups, "ns groups should match")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEngine_MultiplePeers(t *testing.T) {
|
func TestEngine_MultiplePeers(t *testing.T) {
|
||||||
// log.SetLevel(log.DebugLevel)
|
// log.SetLevel(log.DebugLevel)
|
||||||
|
|
||||||
@@ -312,6 +801,7 @@ func TestEngine_MultiplePeers(t *testing.T) {
|
|||||||
t.Errorf("unable to create the engine for peer %d with error %v", j, err)
|
t.Errorf("unable to create the engine for peer %d with error %v", j, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
engine.dnsServer = &dns.MockServer{}
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
err = engine.Start()
|
err = engine.Start()
|
||||||
@@ -391,7 +881,7 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
info := system.GetInfo(ctx)
|
info := system.GetInfo(ctx)
|
||||||
resp, err := mgmtClient.Register(*publicKey, setupKey, "", info)
|
resp, err := mgmtClient.Register(*publicKey, setupKey, "", info, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -411,7 +901,7 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin
|
|||||||
WgPort: wgPort,
|
WgPort: wgPort,
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewEngine(ctx, cancel, signalClient, mgmtClient, conf), nil
|
return NewEngine(ctx, cancel, signalClient, mgmtClient, conf, nbstatus.NewRecorder()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startSignal(port int) (*grpc.Server, error) {
|
func startSignal(port int) (*grpc.Server, error) {
|
||||||
@@ -450,17 +940,17 @@ func startManagement(port int, dataDir string) (*grpc.Server, 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.NewStore(config.Datadir)
|
store, err := server.NewFileStore(config.Datadir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
|
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
|
||||||
}
|
}
|
||||||
peersUpdateManager := server.NewPeersUpdateManager()
|
peersUpdateManager := server.NewPeersUpdateManager()
|
||||||
accountManager, err := server.BuildManager(store, peersUpdateManager, nil)
|
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "")
|
||||||
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)
|
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
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"
|
||||||
@@ -26,13 +26,22 @@ func Login(ctx context.Context, config *Config, setupKey string, jwtToken string
|
|||||||
mgmTlsEnabled = true
|
mgmTlsEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("connecting to Management Service %s", config.ManagementURL.String())
|
log.Debugf("connecting to the Management service %s", config.ManagementURL.String())
|
||||||
mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), err)
|
log.Errorf("failed connecting to the Management service %s %v", config.ManagementURL.String(), err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Debugf("connected to management Service %s", config.ManagementURL.String())
|
log.Debugf("connected to the Management service %s", config.ManagementURL.String())
|
||||||
|
defer func() {
|
||||||
|
err = mgmClient.Close()
|
||||||
|
if err != nil {
|
||||||
|
cStatus, ok := status.FromError(err)
|
||||||
|
if !ok || ok && cStatus.Code() != codes.Canceled {
|
||||||
|
log.Warnf("failed to close the Management service client, err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
serverKey, err := mgmClient.GetServerPublicKey()
|
serverKey, err := mgmClient.GetServerPublicKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -40,15 +49,20 @@ func Login(ctx context.Context, config *Config, setupKey string, jwtToken string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = loginPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken)
|
pubSSHKey, err := ssh.GeneratePublicKey([]byte(config.SSHKey))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = loginPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed logging-in peer on Management Service : %v", err)
|
log.Errorf("failed logging-in peer on Management Service : %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log.Infof("peer has successfully logged-in to the Management service %s", config.ManagementURL.String())
|
||||||
|
|
||||||
err = mgmClient.Close()
|
err = mgmClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed closing Management Service client: %v", err)
|
log.Errorf("failed to close the Management service client: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,26 +70,24 @@ func Login(ctx context.Context, config *Config, setupKey string, jwtToken string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loginPeer attempts to login to Management Service. If peer wasn't registered, tries the registration flow.
|
// loginPeer attempts to login to Management Service. If peer wasn't registered, tries the registration flow.
|
||||||
func loginPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string) (*mgmProto.LoginResponse, error) {
|
func loginPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte) (*mgmProto.LoginResponse, error) {
|
||||||
sysInfo := system.GetInfo(ctx)
|
sysInfo := system.GetInfo(ctx)
|
||||||
loginResp, err := client.Login(serverPublicKey, sysInfo)
|
loginResp, err := client.Login(serverPublicKey, sysInfo, pubSSHKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
||||||
log.Debugf("peer registration required")
|
log.Debugf("peer registration required")
|
||||||
return registerPeer(ctx, serverPublicKey, client, setupKey, jwtToken)
|
return registerPeer(ctx, serverPublicKey, client, setupKey, jwtToken, pubSSHKey)
|
||||||
} else {
|
} else {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("peer has successfully logged-in to Management Service")
|
|
||||||
|
|
||||||
return loginResp, nil
|
return loginResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerPeer checks whether setupKey was provided via cmd line and if not then it prompts user to enter a key.
|
// registerPeer checks whether setupKey was provided via cmd line and if not then it prompts user to enter a key.
|
||||||
// Otherwise tries to register with the provided setupKey via command line.
|
// Otherwise tries to register with the provided setupKey via command line.
|
||||||
func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string) (*mgmProto.LoginResponse, error) {
|
func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte) (*mgmProto.LoginResponse, error) {
|
||||||
validSetupKey, err := uuid.Parse(setupKey)
|
validSetupKey, err := uuid.Parse(setupKey)
|
||||||
if err != nil && jwtToken == "" {
|
if err != nil && jwtToken == "" {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid setup-key or no sso information provided, err: %v", err)
|
return nil, status.Errorf(codes.InvalidArgument, "invalid setup-key or no sso information provided, err: %v", err)
|
||||||
@@ -83,7 +95,7 @@ func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.
|
|||||||
|
|
||||||
log.Debugf("sending peer registration request to Management Service")
|
log.Debugf("sending peer registration request to Management Service")
|
||||||
info := system.GetInfo(ctx)
|
info := system.GetInfo(ctx)
|
||||||
loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info)
|
loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info, pubSSHKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed registering peer %v,%s", err, validSetupKey.String())
|
log.Errorf("failed registering peer %v,%s", err, validSetupKey.String())
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -14,7 +16,6 @@ import (
|
|||||||
// OAuthClient is a OAuth client interface for various idp providers
|
// OAuthClient is a OAuth client interface for various idp providers
|
||||||
type OAuthClient interface {
|
type OAuthClient interface {
|
||||||
RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
|
RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
|
||||||
RotateAccessToken(ctx context.Context, refreshToken string) (TokenInfo, error)
|
|
||||||
WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error)
|
WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error)
|
||||||
GetClientID(ctx context.Context) string
|
GetClientID(ctx context.Context) string
|
||||||
}
|
}
|
||||||
@@ -55,8 +56,10 @@ type Hosted struct {
|
|||||||
Audience string
|
Audience string
|
||||||
// Hosted Native application client id
|
// Hosted Native application client id
|
||||||
ClientID string
|
ClientID string
|
||||||
// Hosted domain
|
// TokenEndpoint to request access token
|
||||||
Domain string
|
TokenEndpoint string
|
||||||
|
// DeviceAuthEndpoint to request device authorization code
|
||||||
|
DeviceAuthEndpoint string
|
||||||
|
|
||||||
HTTPClient HTTPClient
|
HTTPClient HTTPClient
|
||||||
}
|
}
|
||||||
@@ -84,11 +87,11 @@ type TokenRequestResponse struct {
|
|||||||
|
|
||||||
// Claims used when validating the access token
|
// Claims used when validating the access token
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
Audience string `json:"aud"`
|
Audience interface{} `json:"aud"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHostedDeviceFlow returns an Hosted OAuth client
|
// NewHostedDeviceFlow returns an Hosted OAuth client
|
||||||
func NewHostedDeviceFlow(audience string, clientID string, domain string) *Hosted {
|
func NewHostedDeviceFlow(audience string, clientID string, tokenEndpoint string, deviceAuthEndpoint string) *Hosted {
|
||||||
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
httpTransport.MaxIdleConns = 5
|
httpTransport.MaxIdleConns = 5
|
||||||
|
|
||||||
@@ -100,8 +103,9 @@ func NewHostedDeviceFlow(audience string, clientID string, domain string) *Hoste
|
|||||||
return &Hosted{
|
return &Hosted{
|
||||||
Audience: audience,
|
Audience: audience,
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
Domain: domain,
|
TokenEndpoint: tokenEndpoint,
|
||||||
HTTPClient: httpClient,
|
HTTPClient: httpClient,
|
||||||
|
DeviceAuthEndpoint: deviceAuthEndpoint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,22 +116,15 @@ func (h *Hosted) GetClientID(ctx context.Context) string {
|
|||||||
|
|
||||||
// RequestDeviceCode requests a device code login flow information from Hosted
|
// RequestDeviceCode requests a device code login flow information from Hosted
|
||||||
func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error) {
|
func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error) {
|
||||||
url := "https://" + h.Domain + "/oauth/device/code"
|
form := url.Values{}
|
||||||
codePayload := RequestDeviceCodePayload{
|
form.Add("client_id", h.ClientID)
|
||||||
Audience: h.Audience,
|
form.Add("audience", h.Audience)
|
||||||
ClientID: h.ClientID,
|
req, err := http.NewRequest("POST", h.DeviceAuthEndpoint,
|
||||||
}
|
strings.NewReader(form.Encode()))
|
||||||
p, err := json.Marshal(codePayload)
|
|
||||||
if err != nil {
|
|
||||||
return DeviceAuthInfo{}, fmt.Errorf("parsing payload failed with error: %v", err)
|
|
||||||
}
|
|
||||||
payload := strings.NewReader(string(p))
|
|
||||||
req, err := http.NewRequest("POST", url, payload)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return DeviceAuthInfo{}, fmt.Errorf("creating request failed with error: %v", err)
|
return DeviceAuthInfo{}, fmt.Errorf("creating request failed with error: %v", err)
|
||||||
}
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
req.Header.Add("content-type", "application/json")
|
|
||||||
|
|
||||||
res, err := h.HTTPClient.Do(req)
|
res, err := h.HTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -135,7 +132,7 @@ func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return DeviceAuthInfo{}, fmt.Errorf("reading body failed with error: %v", err)
|
return DeviceAuthInfo{}, fmt.Errorf("reading body failed with error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -153,6 +150,48 @@ func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
|
|||||||
return deviceCode, err
|
return deviceCode, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Hosted) requestToken(info DeviceAuthInfo) (TokenRequestResponse, error) {
|
||||||
|
form := url.Values{}
|
||||||
|
form.Add("client_id", h.ClientID)
|
||||||
|
form.Add("grant_type", HostedGrantType)
|
||||||
|
form.Add("device_code", info.DeviceCode)
|
||||||
|
req, err := http.NewRequest("POST", h.TokenEndpoint, strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return TokenRequestResponse{}, fmt.Errorf("failed to create request access token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
res, err := h.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return TokenRequestResponse{}, fmt.Errorf("failed to request access token with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return TokenRequestResponse{}, fmt.Errorf("failed reading access token response body with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode > 499 {
|
||||||
|
return TokenRequestResponse{}, fmt.Errorf("access token response returned code: %s", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResponse := TokenRequestResponse{}
|
||||||
|
err = json.Unmarshal(body, &tokenResponse)
|
||||||
|
if err != nil {
|
||||||
|
return TokenRequestResponse{}, fmt.Errorf("parsing token response failed with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
// WaitToken waits user's login and authorize the app. Once the user's authorize
|
// WaitToken waits user's login and authorize the app. Once the user's authorize
|
||||||
// it retrieves the access token from Hosted's endpoint and validates it before returning
|
// it retrieves the access token from Hosted's endpoint and validates it before returning
|
||||||
func (h *Hosted) WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error) {
|
func (h *Hosted) WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error) {
|
||||||
@@ -163,24 +202,8 @@ func (h *Hosted) WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo,
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return TokenInfo{}, ctx.Err()
|
return TokenInfo{}, ctx.Err()
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
url := "https://" + h.Domain + "/oauth/token"
|
|
||||||
tokenReqPayload := TokenRequestPayload{
|
|
||||||
GrantType: HostedGrantType,
|
|
||||||
DeviceCode: info.DeviceCode,
|
|
||||||
ClientID: h.ClientID,
|
|
||||||
}
|
|
||||||
|
|
||||||
body, statusCode, err := requestToken(h.HTTPClient, url, tokenReqPayload)
|
tokenResponse, err := h.requestToken(info)
|
||||||
if err != nil {
|
|
||||||
return TokenInfo{}, fmt.Errorf("wait for token: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if statusCode > 499 {
|
|
||||||
return TokenInfo{}, fmt.Errorf("wait token code returned error: %s", string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenResponse := TokenRequestResponse{}
|
|
||||||
err = json.Unmarshal(body, &tokenResponse)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return TokenInfo{}, fmt.Errorf("parsing token response failed with error: %v", err)
|
return TokenInfo{}, fmt.Errorf("parsing token response failed with error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -214,71 +237,6 @@ func (h *Hosted) WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RotateAccessToken requests a new token using an existing refresh token
|
|
||||||
func (h *Hosted) RotateAccessToken(ctx context.Context, refreshToken string) (TokenInfo, error) {
|
|
||||||
url := "https://" + h.Domain + "/oauth/token"
|
|
||||||
tokenReqPayload := TokenRequestPayload{
|
|
||||||
GrantType: HostedRefreshGrant,
|
|
||||||
ClientID: h.ClientID,
|
|
||||||
RefreshToken: refreshToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
body, statusCode, err := requestToken(h.HTTPClient, url, tokenReqPayload)
|
|
||||||
if err != nil {
|
|
||||||
return TokenInfo{}, fmt.Errorf("rotate access token: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if statusCode != 200 {
|
|
||||||
return TokenInfo{}, fmt.Errorf("rotating token returned error: %s", string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenResponse := TokenRequestResponse{}
|
|
||||||
err = json.Unmarshal(body, &tokenResponse)
|
|
||||||
if err != nil {
|
|
||||||
return TokenInfo{}, fmt.Errorf("parsing token response failed with error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = isValidAccessToken(tokenResponse.AccessToken, h.Audience)
|
|
||||||
if err != nil {
|
|
||||||
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenInfo := TokenInfo{
|
|
||||||
AccessToken: tokenResponse.AccessToken,
|
|
||||||
TokenType: tokenResponse.TokenType,
|
|
||||||
RefreshToken: tokenResponse.RefreshToken,
|
|
||||||
IDToken: tokenResponse.IDToken,
|
|
||||||
ExpiresIn: tokenResponse.ExpiresIn,
|
|
||||||
}
|
|
||||||
return tokenInfo, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestToken(client HTTPClient, url string, tokenReqPayload TokenRequestPayload) ([]byte, int, error) {
|
|
||||||
p, err := json.Marshal(tokenReqPayload)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, fmt.Errorf("parsing token payload failed with error: %v", err)
|
|
||||||
}
|
|
||||||
payload := strings.NewReader(string(p))
|
|
||||||
req, err := http.NewRequest("POST", url, payload)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, fmt.Errorf("creating token request failed with error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Add("content-type", "application/json")
|
|
||||||
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, fmt.Errorf("doing token request failed with error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer res.Body.Close()
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, fmt.Errorf("reading token body failed with error: %v", err)
|
|
||||||
}
|
|
||||||
return body, res.StatusCode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidAccessToken is a simple validation of the access token
|
// isValidAccessToken is a simple validation of the access token
|
||||||
func isValidAccessToken(token string, audience string) error {
|
func isValidAccessToken(token string, audience string) error {
|
||||||
if token == "" {
|
if token == "" {
|
||||||
@@ -297,9 +255,24 @@ func isValidAccessToken(token string, audience string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.Audience != audience {
|
if claims.Audience == nil {
|
||||||
return fmt.Errorf("invalid audience")
|
return fmt.Errorf("required token field audience is absent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Audience claim of JWT can be a string or an array of strings
|
||||||
|
typ := reflect.TypeOf(claims.Audience)
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
if claims.Audience == audience {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
case reflect.Slice:
|
||||||
|
for _, aud := range claims.Audience.([]interface{}) {
|
||||||
|
if audience == aud {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("invalid JWT token audience field")
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -24,7 +24,7 @@ type mockHTTPClient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||||
body, err := ioutil.ReadAll(req.Body)
|
body, err := io.ReadAll(req.Body)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.reqBody = string(body)
|
c.reqBody = string(body)
|
||||||
}
|
}
|
||||||
@@ -33,13 +33,13 @@ func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
|||||||
c.count++
|
c.count++
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
StatusCode: c.code,
|
StatusCode: c.code,
|
||||||
Body: ioutil.NopCloser(strings.NewReader(c.countResBody)),
|
Body: io.NopCloser(strings.NewReader(c.countResBody)),
|
||||||
}, c.err
|
}, c.err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
StatusCode: c.code,
|
StatusCode: c.code,
|
||||||
Body: ioutil.NopCloser(strings.NewReader(c.resBody)),
|
Body: io.NopCloser(strings.NewReader(c.resBody)),
|
||||||
}, c.err
|
}, c.err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,15 +54,19 @@ func TestHosted_RequestDeviceCode(t *testing.T) {
|
|||||||
testingFunc require.ComparisonAssertionFunc
|
testingFunc require.ComparisonAssertionFunc
|
||||||
expectedOut DeviceAuthInfo
|
expectedOut DeviceAuthInfo
|
||||||
expectedMSG string
|
expectedMSG string
|
||||||
expectPayload RequestDeviceCodePayload
|
expectPayload string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectedAudience := "ok"
|
||||||
|
expectedClientID := "bla"
|
||||||
|
form := url.Values{}
|
||||||
|
form.Add("audience", expectedAudience)
|
||||||
|
form.Add("client_id", expectedClientID)
|
||||||
|
expectPayload := form.Encode()
|
||||||
|
|
||||||
testCase1 := test{
|
testCase1 := test{
|
||||||
name: "Payload Is Valid",
|
name: "Payload Is Valid",
|
||||||
expectPayload: RequestDeviceCodePayload{
|
expectPayload: expectPayload,
|
||||||
Audience: "ok",
|
|
||||||
ClientID: "bla",
|
|
||||||
},
|
|
||||||
inputReqCode: 200,
|
inputReqCode: 200,
|
||||||
testingErrFunc: require.Error,
|
testingErrFunc: require.Error,
|
||||||
testingFunc: require.EqualValues,
|
testingFunc: require.EqualValues,
|
||||||
@@ -74,6 +78,7 @@ func TestHosted_RequestDeviceCode(t *testing.T) {
|
|||||||
testingErrFunc: require.Error,
|
testingErrFunc: require.Error,
|
||||||
expectedErrorMSG: "should return error",
|
expectedErrorMSG: "should return error",
|
||||||
testingFunc: require.EqualValues,
|
testingFunc: require.EqualValues,
|
||||||
|
expectPayload: expectPayload,
|
||||||
}
|
}
|
||||||
|
|
||||||
testCase3 := test{
|
testCase3 := test{
|
||||||
@@ -82,15 +87,13 @@ func TestHosted_RequestDeviceCode(t *testing.T) {
|
|||||||
testingErrFunc: require.Error,
|
testingErrFunc: require.Error,
|
||||||
expectedErrorMSG: "should return error",
|
expectedErrorMSG: "should return error",
|
||||||
testingFunc: require.EqualValues,
|
testingFunc: require.EqualValues,
|
||||||
|
expectPayload: expectPayload,
|
||||||
}
|
}
|
||||||
testCase4Out := DeviceAuthInfo{ExpiresIn: 10}
|
testCase4Out := DeviceAuthInfo{ExpiresIn: 10}
|
||||||
testCase4 := test{
|
testCase4 := test{
|
||||||
name: "Got Device Code",
|
name: "Got Device Code",
|
||||||
inputResBody: fmt.Sprintf("{\"expires_in\":%d}", testCase4Out.ExpiresIn),
|
inputResBody: fmt.Sprintf("{\"expires_in\":%d}", testCase4Out.ExpiresIn),
|
||||||
expectPayload: RequestDeviceCodePayload{
|
expectPayload: expectPayload,
|
||||||
Audience: "ok",
|
|
||||||
ClientID: "bla",
|
|
||||||
},
|
|
||||||
inputReqCode: 200,
|
inputReqCode: 200,
|
||||||
testingErrFunc: require.NoError,
|
testingErrFunc: require.NoError,
|
||||||
testingFunc: require.EqualValues,
|
testingFunc: require.EqualValues,
|
||||||
@@ -108,18 +111,17 @@ func TestHosted_RequestDeviceCode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hosted := Hosted{
|
hosted := Hosted{
|
||||||
Audience: testCase.expectPayload.Audience,
|
Audience: expectedAudience,
|
||||||
ClientID: testCase.expectPayload.ClientID,
|
ClientID: expectedClientID,
|
||||||
Domain: "test.hosted.com",
|
TokenEndpoint: "test.hosted.com/token",
|
||||||
|
DeviceAuthEndpoint: "test.hosted.com/device/auth",
|
||||||
HTTPClient: &httpClient,
|
HTTPClient: &httpClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
authInfo, err := hosted.RequestDeviceCode(context.TODO())
|
authInfo, err := hosted.RequestDeviceCode(context.TODO())
|
||||||
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
|
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
|
||||||
|
|
||||||
payload, _ := json.Marshal(testCase.expectPayload)
|
require.EqualValues(t, expectPayload, httpClient.reqBody, "payload should match")
|
||||||
|
|
||||||
require.EqualValues(t, string(payload), httpClient.reqBody, "payload should match")
|
|
||||||
|
|
||||||
testCase.testingFunc(t, testCase.expectedOut, authInfo, testCase.expectedMSG)
|
testCase.testingFunc(t, testCase.expectedOut, authInfo, testCase.expectedMSG)
|
||||||
|
|
||||||
@@ -143,7 +145,7 @@ func TestHosted_WaitToken(t *testing.T) {
|
|||||||
testingFunc require.ComparisonAssertionFunc
|
testingFunc require.ComparisonAssertionFunc
|
||||||
expectedOut TokenInfo
|
expectedOut TokenInfo
|
||||||
expectedMSG string
|
expectedMSG string
|
||||||
expectPayload TokenRequestPayload
|
expectPayload string
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultInfo := DeviceAuthInfo{
|
defaultInfo := DeviceAuthInfo{
|
||||||
@@ -152,11 +154,13 @@ func TestHosted_WaitToken(t *testing.T) {
|
|||||||
Interval: 1,
|
Interval: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenReqPayload := TokenRequestPayload{
|
clientID := "test"
|
||||||
GrantType: HostedGrantType,
|
|
||||||
DeviceCode: defaultInfo.DeviceCode,
|
form := url.Values{}
|
||||||
ClientID: "test",
|
form.Add("grant_type", HostedGrantType)
|
||||||
}
|
form.Add("device_code", defaultInfo.DeviceCode)
|
||||||
|
form.Add("client_id", clientID)
|
||||||
|
tokenReqPayload := form.Encode()
|
||||||
|
|
||||||
testCase1 := test{
|
testCase1 := test{
|
||||||
name: "Payload Is Valid",
|
name: "Payload Is Valid",
|
||||||
@@ -269,8 +273,9 @@ func TestHosted_WaitToken(t *testing.T) {
|
|||||||
|
|
||||||
hosted := Hosted{
|
hosted := Hosted{
|
||||||
Audience: testCase.inputAudience,
|
Audience: testCase.inputAudience,
|
||||||
ClientID: testCase.expectPayload.ClientID,
|
ClientID: clientID,
|
||||||
Domain: "test.hosted.com",
|
TokenEndpoint: "test.hosted.com/token",
|
||||||
|
DeviceAuthEndpoint: "test.hosted.com/device/auth",
|
||||||
HTTPClient: &httpClient,
|
HTTPClient: &httpClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,12 +284,7 @@ func TestHosted_WaitToken(t *testing.T) {
|
|||||||
tokenInfo, err := hosted.WaitToken(ctx, testCase.inputInfo)
|
tokenInfo, err := hosted.WaitToken(ctx, testCase.inputInfo)
|
||||||
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
|
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
|
||||||
|
|
||||||
var payload []byte
|
require.EqualValues(t, testCase.expectPayload, httpClient.reqBody, "payload should match")
|
||||||
var emptyPayload TokenRequestPayload
|
|
||||||
if testCase.expectPayload != emptyPayload {
|
|
||||||
payload, _ = json.Marshal(testCase.expectPayload)
|
|
||||||
}
|
|
||||||
require.EqualValues(t, string(payload), httpClient.reqBody, "payload should match")
|
|
||||||
|
|
||||||
testCase.testingFunc(t, testCase.expectedOut, tokenInfo, testCase.expectedMSG)
|
testCase.testingFunc(t, testCase.expectedOut, tokenInfo, testCase.expectedMSG)
|
||||||
|
|
||||||
@@ -293,123 +293,3 @@ func TestHosted_WaitToken(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHosted_RotateAccessToken(t *testing.T) {
|
|
||||||
type test struct {
|
|
||||||
name string
|
|
||||||
inputResBody string
|
|
||||||
inputReqCode int
|
|
||||||
inputReqError error
|
|
||||||
inputMaxReqs int
|
|
||||||
inputInfo DeviceAuthInfo
|
|
||||||
inputAudience string
|
|
||||||
testingErrFunc require.ErrorAssertionFunc
|
|
||||||
expectedErrorMSG string
|
|
||||||
testingFunc require.ComparisonAssertionFunc
|
|
||||||
expectedOut TokenInfo
|
|
||||||
expectedMSG string
|
|
||||||
expectPayload TokenRequestPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultInfo := DeviceAuthInfo{
|
|
||||||
DeviceCode: "test",
|
|
||||||
ExpiresIn: 10,
|
|
||||||
Interval: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenReqPayload := TokenRequestPayload{
|
|
||||||
GrantType: HostedRefreshGrant,
|
|
||||||
ClientID: "test",
|
|
||||||
RefreshToken: "refresh_test",
|
|
||||||
}
|
|
||||||
|
|
||||||
testCase1 := test{
|
|
||||||
name: "Payload Is Valid",
|
|
||||||
inputInfo: defaultInfo,
|
|
||||||
inputReqCode: 200,
|
|
||||||
testingErrFunc: require.Error,
|
|
||||||
testingFunc: require.EqualValues,
|
|
||||||
expectPayload: tokenReqPayload,
|
|
||||||
}
|
|
||||||
|
|
||||||
testCase2 := test{
|
|
||||||
name: "Exit On Network Error",
|
|
||||||
inputInfo: defaultInfo,
|
|
||||||
expectPayload: tokenReqPayload,
|
|
||||||
inputReqError: fmt.Errorf("error"),
|
|
||||||
testingErrFunc: require.Error,
|
|
||||||
expectedErrorMSG: "should return error",
|
|
||||||
testingFunc: require.EqualValues,
|
|
||||||
}
|
|
||||||
|
|
||||||
testCase3 := test{
|
|
||||||
name: "Exit On Non 200 Status Code",
|
|
||||||
inputInfo: defaultInfo,
|
|
||||||
inputReqCode: 401,
|
|
||||||
expectPayload: tokenReqPayload,
|
|
||||||
testingErrFunc: require.Error,
|
|
||||||
expectedErrorMSG: "should return error",
|
|
||||||
testingFunc: require.EqualValues,
|
|
||||||
}
|
|
||||||
|
|
||||||
audience := "test"
|
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"aud": audience})
|
|
||||||
var hmacSampleSecret []byte
|
|
||||||
tokenString, _ := token.SignedString(hmacSampleSecret)
|
|
||||||
|
|
||||||
testCase4 := test{
|
|
||||||
name: "Exit On Invalid Audience",
|
|
||||||
inputInfo: defaultInfo,
|
|
||||||
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\"}", tokenString),
|
|
||||||
inputReqCode: 200,
|
|
||||||
inputAudience: "super test",
|
|
||||||
testingErrFunc: require.Error,
|
|
||||||
testingFunc: require.EqualValues,
|
|
||||||
expectPayload: tokenReqPayload,
|
|
||||||
}
|
|
||||||
|
|
||||||
testCase5 := test{
|
|
||||||
name: "Received Token Info",
|
|
||||||
inputInfo: defaultInfo,
|
|
||||||
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\"}", tokenString),
|
|
||||||
inputReqCode: 200,
|
|
||||||
inputAudience: audience,
|
|
||||||
testingErrFunc: require.NoError,
|
|
||||||
testingFunc: require.EqualValues,
|
|
||||||
expectPayload: tokenReqPayload,
|
|
||||||
expectedOut: TokenInfo{AccessToken: tokenString},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5} {
|
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
|
||||||
|
|
||||||
httpClient := mockHTTPClient{
|
|
||||||
resBody: testCase.inputResBody,
|
|
||||||
code: testCase.inputReqCode,
|
|
||||||
err: testCase.inputReqError,
|
|
||||||
MaxReqs: testCase.inputMaxReqs,
|
|
||||||
}
|
|
||||||
|
|
||||||
hosted := Hosted{
|
|
||||||
Audience: testCase.inputAudience,
|
|
||||||
ClientID: testCase.expectPayload.ClientID,
|
|
||||||
Domain: "test.hosted.com",
|
|
||||||
HTTPClient: &httpClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenInfo, err := hosted.RotateAccessToken(context.TODO(), testCase.expectPayload.RefreshToken)
|
|
||||||
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
|
|
||||||
|
|
||||||
var payload []byte
|
|
||||||
var emptyPayload TokenRequestPayload
|
|
||||||
if testCase.expectPayload != emptyPayload {
|
|
||||||
payload, _ = json.Marshal(testCase.expectPayload)
|
|
||||||
}
|
|
||||||
require.EqualValues(t, string(payload), httpClient.reqBody, "payload should match")
|
|
||||||
|
|
||||||
testCase.testingFunc(t, testCase.expectedOut, tokenInfo, testCase.expectedMSG)
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ package peer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/netbirdio/netbird/iface"
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl"
|
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/proxy"
|
"github.com/netbirdio/netbird/client/internal/proxy"
|
||||||
|
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
"github.com/pion/ice/v2"
|
"github.com/pion/ice/v2"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnConfig is a peer Connection configuration
|
// ConnConfig is a peer Connection configuration
|
||||||
@@ -27,6 +30,7 @@ type ConnConfig struct {
|
|||||||
// InterfaceBlackList is a list of machine interfaces that should be filtered out by ICE Candidate gathering
|
// 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)
|
// (e.g. if eth0 is in the list, host candidate of this interface won't be used)
|
||||||
InterfaceBlackList []string
|
InterfaceBlackList []string
|
||||||
|
DisableIPv6Discovery bool
|
||||||
|
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
|
|
||||||
@@ -34,6 +38,22 @@ type ConnConfig struct {
|
|||||||
|
|
||||||
UDPMux ice.UDPMux
|
UDPMux ice.UDPMux
|
||||||
UDPMuxSrflx ice.UniversalUDPMux
|
UDPMuxSrflx ice.UniversalUDPMux
|
||||||
|
|
||||||
|
LocalWgPort int
|
||||||
|
|
||||||
|
NATExternalIPs []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
|
||||||
}
|
}
|
||||||
|
|
||||||
// IceCredentials ICE protocol credentials struct
|
// IceCredentials ICE protocol credentials struct
|
||||||
@@ -49,13 +69,13 @@ type Conn struct {
|
|||||||
// signalCandidate is a handler function to signal remote peer about local connection candidate
|
// signalCandidate is a handler function to signal remote peer about local connection candidate
|
||||||
signalCandidate func(candidate ice.Candidate) error
|
signalCandidate func(candidate ice.Candidate) error
|
||||||
// signalOffer is a handler function to signal remote peer our connection offer (credentials)
|
// signalOffer is a handler function to signal remote peer our connection offer (credentials)
|
||||||
signalOffer func(uFrag string, pwd string) error
|
signalOffer func(OfferAnswer) error
|
||||||
signalAnswer func(uFrag string, pwd string) error
|
signalAnswer func(OfferAnswer) error
|
||||||
|
|
||||||
// remoteOffersCh is a channel used to wait for remote credentials to proceed with the connection
|
// remoteOffersCh is a channel used to wait for remote credentials to proceed with the connection
|
||||||
remoteOffersCh chan IceCredentials
|
remoteOffersCh chan OfferAnswer
|
||||||
// remoteAnswerCh is a channel used to wait for remote credentials answer (confirmation of our offer) to proceed with the connection
|
// remoteAnswerCh is a channel used to wait for remote credentials answer (confirmation of our offer) to proceed with the connection
|
||||||
remoteAnswerCh chan IceCredentials
|
remoteAnswerCh chan OfferAnswer
|
||||||
closeCh chan struct{}
|
closeCh chan struct{}
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
notifyDisconnected context.CancelFunc
|
notifyDisconnected context.CancelFunc
|
||||||
@@ -63,43 +83,57 @@ type Conn struct {
|
|||||||
agent *ice.Agent
|
agent *ice.Agent
|
||||||
status ConnStatus
|
status ConnStatus
|
||||||
|
|
||||||
|
statusRecorder *nbStatus.Status
|
||||||
|
|
||||||
proxy proxy.Proxy
|
proxy proxy.Proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConf returns the connection config
|
||||||
|
func (conn *Conn) GetConf() ConnConfig {
|
||||||
|
return conn.config
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateConf updates the connection config
|
||||||
|
func (conn *Conn) UpdateConf(conf ConnConfig) {
|
||||||
|
conn.config = conf
|
||||||
|
}
|
||||||
|
|
||||||
// NewConn creates a new not opened Conn to the remote peer.
|
// NewConn creates a new not opened Conn to the remote peer.
|
||||||
// To establish a connection run Conn.Open
|
// To establish a connection run Conn.Open
|
||||||
func NewConn(config ConnConfig) (*Conn, error) {
|
func NewConn(config ConnConfig, statusRecorder *nbStatus.Status) (*Conn, error) {
|
||||||
return &Conn{
|
return &Conn{
|
||||||
config: config,
|
config: config,
|
||||||
mu: sync.Mutex{},
|
mu: sync.Mutex{},
|
||||||
status: StatusDisconnected,
|
status: StatusDisconnected,
|
||||||
closeCh: make(chan struct{}),
|
closeCh: make(chan struct{}),
|
||||||
remoteOffersCh: make(chan IceCredentials),
|
remoteOffersCh: make(chan OfferAnswer),
|
||||||
remoteAnswerCh: make(chan IceCredentials),
|
remoteAnswerCh: make(chan OfferAnswer),
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// interfaceFilter is a function passed to ICE Agent to filter out blacklisted interfaces
|
// interfaceFilter is a function passed to ICE Agent to filter out not allowed interfaces
|
||||||
|
// to avoid building tunnel over them
|
||||||
func interfaceFilter(blackList []string) func(string) bool {
|
func interfaceFilter(blackList []string) func(string) bool {
|
||||||
var blackListMap map[string]struct{}
|
|
||||||
if blackList != nil {
|
|
||||||
blackListMap = make(map[string]struct{})
|
|
||||||
for _, s := range blackList {
|
|
||||||
blackListMap[s] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return func(iFace string) bool {
|
|
||||||
|
|
||||||
_, ok := blackListMap[iFace]
|
return func(iFace string) bool {
|
||||||
if ok {
|
for _, s := range blackList {
|
||||||
|
if strings.HasPrefix(iFace, s) {
|
||||||
|
log.Debugf("ignoring interface %s - it is not allowed", iFace)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// look for unlisted Wireguard interfaces
|
}
|
||||||
|
// look for unlisted WireGuard interfaces
|
||||||
wg, err := wgctrl.New()
|
wg, err := wgctrl.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("trying to create a wgctrl client failed with: %v", err)
|
log.Debugf("trying to create a wgctrl client failed with: %v", err)
|
||||||
}
|
}
|
||||||
defer wg.Close()
|
defer func() {
|
||||||
|
err := wg.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
_, err = wg.Device(iFace)
|
_, err = wg.Device(iFace)
|
||||||
return err != nil
|
return err != nil
|
||||||
@@ -112,16 +146,24 @@ func (conn *Conn) reCreateAgent() error {
|
|||||||
|
|
||||||
failedTimeout := 6 * time.Second
|
failedTimeout := 6 * time.Second
|
||||||
var err error
|
var err error
|
||||||
conn.agent, err = ice.NewAgent(&ice.AgentConfig{
|
agentConfig := &ice.AgentConfig{
|
||||||
MulticastDNSMode: ice.MulticastDNSModeDisabled,
|
MulticastDNSMode: ice.MulticastDNSModeDisabled,
|
||||||
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4},
|
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6},
|
||||||
Urls: conn.config.StunTurn,
|
Urls: conn.config.StunTurn,
|
||||||
CandidateTypes: []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive, ice.CandidateTypeRelay},
|
CandidateTypes: []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive, ice.CandidateTypeRelay},
|
||||||
FailedTimeout: &failedTimeout,
|
FailedTimeout: &failedTimeout,
|
||||||
InterfaceFilter: interfaceFilter(conn.config.InterfaceBlackList),
|
InterfaceFilter: interfaceFilter(conn.config.InterfaceBlackList),
|
||||||
UDPMux: conn.config.UDPMux,
|
UDPMux: conn.config.UDPMux,
|
||||||
UDPMuxSrflx: conn.config.UDPMuxSrflx,
|
UDPMuxSrflx: conn.config.UDPMuxSrflx,
|
||||||
})
|
NAT1To1IPs: conn.config.NATExternalIPs,
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn.config.DisableIPv6Discovery {
|
||||||
|
agentConfig.NetworkTypes = []ice.NetworkType{ice.NetworkTypeUDP4}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.agent, err = ice.NewAgent(agentConfig)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -150,6 +192,17 @@ func (conn *Conn) reCreateAgent() error {
|
|||||||
func (conn *Conn) Open() error {
|
func (conn *Conn) Open() error {
|
||||||
log.Debugf("trying to connect to peer %s", conn.config.Key)
|
log.Debugf("trying to connect to peer %s", conn.config.Key)
|
||||||
|
|
||||||
|
peerState := nbStatus.PeerState{PubKey: conn.config.Key}
|
||||||
|
|
||||||
|
peerState.IP = strings.Split(conn.config.ProxyConfig.AllowedIps, "/")[0]
|
||||||
|
peerState.ConnStatusUpdate = time.Now()
|
||||||
|
peerState.ConnStatus = conn.status.String()
|
||||||
|
|
||||||
|
err := conn.statusRecorder.UpdatePeerState(peerState)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("erro while updating the state of peer %s,err: %v", conn.config.Key, err)
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := conn.cleanup()
|
err := conn.cleanup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -158,7 +211,7 @@ func (conn *Conn) Open() error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err := conn.reCreateAgent()
|
err = conn.reCreateAgent()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -173,15 +226,15 @@ func (conn *Conn) Open() error {
|
|||||||
// Only continue once we got a connection confirmation from the remote peer.
|
// Only continue once we got a connection confirmation from the remote peer.
|
||||||
// The connection timeout could have happened before a confirmation received from the remote.
|
// The connection timeout could have happened before a confirmation received from the remote.
|
||||||
// The connection could have also been closed externally (e.g. when we received an update from the management that peer shouldn't be connected)
|
// The connection could have also been closed externally (e.g. when we received an update from the management that peer shouldn't be connected)
|
||||||
var remoteCredentials IceCredentials
|
var remoteOfferAnswer OfferAnswer
|
||||||
select {
|
select {
|
||||||
case remoteCredentials = <-conn.remoteOffersCh:
|
case remoteOfferAnswer = <-conn.remoteOffersCh:
|
||||||
// received confirmation from the remote peer -> ready to proceed
|
// received confirmation from the remote peer -> ready to proceed
|
||||||
err = conn.sendAnswer()
|
err = conn.sendAnswer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case remoteCredentials = <-conn.remoteAnswerCh:
|
case remoteOfferAnswer = <-conn.remoteAnswerCh:
|
||||||
case <-time.After(conn.config.Timeout):
|
case <-time.After(conn.config.Timeout):
|
||||||
return NewConnectionTimeoutError(conn.config.Key, conn.config.Timeout)
|
return NewConnectionTimeoutError(conn.config.Key, conn.config.Timeout)
|
||||||
case <-conn.closeCh:
|
case <-conn.closeCh:
|
||||||
@@ -189,7 +242,8 @@ func (conn *Conn) Open() error {
|
|||||||
return NewConnectionClosedError(conn.config.Key)
|
return NewConnectionClosedError(conn.config.Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("received connection confirmation from peer %s", conn.config.Key)
|
log.Debugf("received connection confirmation from peer %s running version %s and with remote WireGuard listen port %d",
|
||||||
|
conn.config.Key, remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort)
|
||||||
|
|
||||||
// at this point we received offer/answer and we are ready to gather candidates
|
// at this point we received offer/answer and we are ready to gather candidates
|
||||||
conn.mu.Lock()
|
conn.mu.Lock()
|
||||||
@@ -198,6 +252,15 @@ func (conn *Conn) Open() error {
|
|||||||
defer conn.notifyDisconnected()
|
defer conn.notifyDisconnected()
|
||||||
conn.mu.Unlock()
|
conn.mu.Unlock()
|
||||||
|
|
||||||
|
peerState = nbStatus.PeerState{PubKey: conn.config.Key}
|
||||||
|
|
||||||
|
peerState.ConnStatus = conn.status.String()
|
||||||
|
peerState.ConnStatusUpdate = time.Now()
|
||||||
|
err = conn.statusRecorder.UpdatePeerState(peerState)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("erro while updating the state of peer %s,err: %v", conn.config.Key, err)
|
||||||
|
}
|
||||||
|
|
||||||
err = conn.agent.GatherCandidates()
|
err = conn.agent.GatherCandidates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -209,16 +272,21 @@ func (conn *Conn) Open() error {
|
|||||||
isControlling := conn.config.LocalKey > conn.config.Key
|
isControlling := conn.config.LocalKey > conn.config.Key
|
||||||
var remoteConn *ice.Conn
|
var remoteConn *ice.Conn
|
||||||
if isControlling {
|
if isControlling {
|
||||||
remoteConn, err = conn.agent.Dial(conn.ctx, remoteCredentials.UFrag, remoteCredentials.Pwd)
|
remoteConn, err = conn.agent.Dial(conn.ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
|
||||||
} else {
|
} else {
|
||||||
remoteConn, err = conn.agent.Accept(conn.ctx, remoteCredentials.UFrag, remoteCredentials.Pwd)
|
remoteConn, err = conn.agent.Accept(conn.ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// the connection has been established successfully so we are ready to start the proxy
|
// dynamically set remote WireGuard port is other side specified a different one from the default one
|
||||||
err = conn.startProxy(remoteConn)
|
remoteWgPort := iface.DefaultWgPort
|
||||||
|
if remoteOfferAnswer.WgListenPort != 0 {
|
||||||
|
remoteWgPort = remoteOfferAnswer.WgListenPort
|
||||||
|
}
|
||||||
|
// the ice connection has been established successfully so we are ready to start the proxy
|
||||||
|
err = conn.startProxy(remoteConn, remoteWgPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -227,7 +295,7 @@ func (conn *Conn) Open() error {
|
|||||||
host, _, _ := net.SplitHostPort(remoteConn.LocalAddr().String())
|
host, _, _ := net.SplitHostPort(remoteConn.LocalAddr().String())
|
||||||
rhost, _, _ := net.SplitHostPort(remoteConn.RemoteAddr().String())
|
rhost, _, _ := net.SplitHostPort(remoteConn.RemoteAddr().String())
|
||||||
// direct Wireguard connection
|
// direct Wireguard connection
|
||||||
log.Infof("directly connected to peer %s [laddr <-> raddr] [%s:%d <-> %s:%d]", conn.config.Key, host, iface.DefaultWgPort, rhost, iface.DefaultWgPort)
|
log.Infof("directly connected to peer %s [laddr <-> raddr] [%s:%d <-> %s:%d]", conn.config.Key, host, conn.config.LocalWgPort, rhost, remoteWgPort)
|
||||||
} else {
|
} else {
|
||||||
log.Infof("connected to peer %s [laddr <-> raddr] [%s <-> %s]", conn.config.Key, remoteConn.LocalAddr().String(), remoteConn.RemoteAddr().String())
|
log.Infof("connected to peer %s [laddr <-> raddr] [%s <-> %s]", conn.config.Key, remoteConn.LocalAddr().String(), remoteConn.RemoteAddr().String())
|
||||||
}
|
}
|
||||||
@@ -252,6 +320,10 @@ func shouldUseProxy(pair *ice.CandidatePair) bool {
|
|||||||
remoteIsPublic := IsPublicIP(remoteIP)
|
remoteIsPublic := IsPublicIP(remoteIP)
|
||||||
myIsPublic := IsPublicIP(myIp)
|
myIsPublic := IsPublicIP(myIp)
|
||||||
|
|
||||||
|
if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
//one of the hosts has a public IP
|
//one of the hosts has a public IP
|
||||||
if remoteIsPublic && pair.Remote.Type() == ice.CandidateTypeHost {
|
if remoteIsPublic && pair.Remote.Type() == ice.CandidateTypeHost {
|
||||||
return false
|
return false
|
||||||
@@ -279,7 +351,7 @@ func IsPublicIP(ip net.IP) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startProxy starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
|
// startProxy starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
|
||||||
func (conn *Conn) startProxy(remoteConn net.Conn) error {
|
func (conn *Conn) startProxy(remoteConn net.Conn, remoteWgPort int) error {
|
||||||
conn.mu.Lock()
|
conn.mu.Lock()
|
||||||
defer conn.mu.Unlock()
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
@@ -289,12 +361,15 @@ func (conn *Conn) startProxy(remoteConn net.Conn) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peerState := nbStatus.PeerState{PubKey: conn.config.Key}
|
||||||
useProxy := shouldUseProxy(pair)
|
useProxy := shouldUseProxy(pair)
|
||||||
var p proxy.Proxy
|
var p proxy.Proxy
|
||||||
if useProxy {
|
if useProxy {
|
||||||
p = proxy.NewWireguardProxy(conn.config.ProxyConfig)
|
p = proxy.NewWireguardProxy(conn.config.ProxyConfig)
|
||||||
|
peerState.Direct = false
|
||||||
} else {
|
} else {
|
||||||
p = proxy.NewNoProxy(conn.config.ProxyConfig)
|
p = proxy.NewNoProxy(conn.config.ProxyConfig, remoteWgPort)
|
||||||
|
peerState.Direct = true
|
||||||
}
|
}
|
||||||
conn.proxy = p
|
conn.proxy = p
|
||||||
err = p.Start(remoteConn)
|
err = p.Start(remoteConn)
|
||||||
@@ -304,6 +379,19 @@ func (conn *Conn) startProxy(remoteConn net.Conn) error {
|
|||||||
|
|
||||||
conn.status = StatusConnected
|
conn.status = StatusConnected
|
||||||
|
|
||||||
|
peerState.ConnStatus = conn.status.String()
|
||||||
|
peerState.ConnStatusUpdate = time.Now()
|
||||||
|
peerState.LocalIceCandidateType = pair.Local.Type().String()
|
||||||
|
peerState.RemoteIceCandidateType = pair.Remote.Type().String()
|
||||||
|
if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay {
|
||||||
|
peerState.Relayed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.statusRecorder.UpdatePeerState(peerState)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("unable to save peer's state, got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,18 +424,29 @@ func (conn *Conn) cleanup() error {
|
|||||||
|
|
||||||
conn.status = StatusDisconnected
|
conn.status = StatusDisconnected
|
||||||
|
|
||||||
|
peerState := nbStatus.PeerState{PubKey: conn.config.Key}
|
||||||
|
peerState.ConnStatus = conn.status.String()
|
||||||
|
peerState.ConnStatusUpdate = time.Now()
|
||||||
|
|
||||||
|
err := conn.statusRecorder.UpdatePeerState(peerState)
|
||||||
|
if err != nil {
|
||||||
|
// pretty common error because by that time Engine can already remove the peer and status won't be available.
|
||||||
|
//todo rethink status updates
|
||||||
|
log.Debugf("error while updating peer's %s state, err: %v", conn.config.Key, err)
|
||||||
|
}
|
||||||
|
|
||||||
log.Debugf("cleaned up connection to peer %s", conn.config.Key)
|
log.Debugf("cleaned up connection to peer %s", conn.config.Key)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSignalOffer sets a handler function to be triggered by Conn when a new connection offer has to be signalled to the remote peer
|
// SetSignalOffer sets a handler function to be triggered by Conn when a new connection offer has to be signalled to the remote peer
|
||||||
func (conn *Conn) SetSignalOffer(handler func(uFrag string, pwd string) error) {
|
func (conn *Conn) SetSignalOffer(handler func(offer OfferAnswer) error) {
|
||||||
conn.signalOffer = handler
|
conn.signalOffer = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSignalAnswer sets a handler function to be triggered by Conn when a new connection answer has to be signalled to the remote peer
|
// SetSignalAnswer sets a handler function to be triggered by Conn when a new connection answer has to be signalled to the remote peer
|
||||||
func (conn *Conn) SetSignalAnswer(handler func(uFrag string, pwd string) error) {
|
func (conn *Conn) SetSignalAnswer(handler func(answer OfferAnswer) error) {
|
||||||
conn.signalAnswer = handler
|
conn.signalAnswer = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +459,8 @@ func (conn *Conn) SetSignalCandidate(handler func(candidate ice.Candidate) error
|
|||||||
// and then signals them to the remote peer
|
// and then signals them to the remote peer
|
||||||
func (conn *Conn) onICECandidate(candidate ice.Candidate) {
|
func (conn *Conn) onICECandidate(candidate ice.Candidate) {
|
||||||
if candidate != nil {
|
if candidate != nil {
|
||||||
// log.Debugf("discovered local candidate %s", candidate.String())
|
// TODO: reported port is incorrect for CandidateTypeHost, makes understanding ICE use via logs confusing as port is ignored
|
||||||
|
log.Debugf("discovered local candidate %s", candidate.String())
|
||||||
go func() {
|
go func() {
|
||||||
err := conn.signalCandidate(candidate)
|
err := conn.signalCandidate(candidate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -392,8 +492,12 @@ func (conn *Conn) sendAnswer() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("sending asnwer to %s", conn.config.Key)
|
log.Debugf("sending answer to %s", conn.config.Key)
|
||||||
err = conn.signalAnswer(localUFrag, localPwd)
|
err = conn.signalAnswer(OfferAnswer{
|
||||||
|
IceCredentials: IceCredentials{localUFrag, localPwd},
|
||||||
|
WgListenPort: conn.config.LocalWgPort,
|
||||||
|
Version: system.NetbirdVersion(),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -410,7 +514,11 @@ func (conn *Conn) sendOffer() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = conn.signalOffer(localUFrag, localPwd)
|
err = conn.signalOffer(OfferAnswer{
|
||||||
|
IceCredentials: IceCredentials{localUFrag, localPwd},
|
||||||
|
WgListenPort: conn.config.LocalWgPort,
|
||||||
|
Version: system.NetbirdVersion(),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -437,7 +545,7 @@ func (conn *Conn) Close() error {
|
|||||||
// before conn.Open() another update from management arrives with peers: [1,2,3,4,5]
|
// before conn.Open() another update from management arrives with peers: [1,2,3,4,5]
|
||||||
// engine adds a new Conn for 4 and 5
|
// engine adds a new Conn for 4 and 5
|
||||||
// therefore peer 4 has 2 Conn objects
|
// therefore peer 4 has 2 Conn objects
|
||||||
log.Warnf("closing not started coonection %s", conn.config.Key)
|
log.Warnf("connection has been already closed or attempted closing not started coonection %s", conn.config.Key)
|
||||||
return NewConnectionAlreadyClosed(conn.config.Key)
|
return NewConnectionAlreadyClosed(conn.config.Key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -451,11 +559,11 @@ func (conn *Conn) Status() ConnStatus {
|
|||||||
|
|
||||||
// OnRemoteOffer handles an offer from the remote peer and returns true if the message was accepted, false otherwise
|
// 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
|
// doesn't block, discards the message if connection wasn't ready
|
||||||
func (conn *Conn) OnRemoteOffer(remoteAuth IceCredentials) bool {
|
func (conn *Conn) OnRemoteOffer(offer OfferAnswer) bool {
|
||||||
log.Debugf("OnRemoteOffer from peer %s on status %s", conn.config.Key, conn.status.String())
|
log.Debugf("OnRemoteOffer from peer %s on status %s", conn.config.Key, conn.status.String())
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case conn.remoteOffersCh <- remoteAuth:
|
case conn.remoteOffersCh <- offer:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
log.Debugf("OnRemoteOffer skipping message from peer %s on status %s because is not ready", conn.config.Key, conn.status.String())
|
log.Debugf("OnRemoteOffer skipping message from peer %s on status %s because is not ready", conn.config.Key, conn.status.String())
|
||||||
@@ -466,11 +574,11 @@ func (conn *Conn) OnRemoteOffer(remoteAuth IceCredentials) bool {
|
|||||||
|
|
||||||
// OnRemoteAnswer handles an offer from the remote peer and returns true if the message was accepted, false otherwise
|
// 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
|
// doesn't block, discards the message if connection wasn't ready
|
||||||
func (conn *Conn) OnRemoteAnswer(remoteAuth IceCredentials) bool {
|
func (conn *Conn) OnRemoteAnswer(answer OfferAnswer) bool {
|
||||||
log.Debugf("OnRemoteAnswer from peer %s on status %s", conn.config.Key, conn.status.String())
|
log.Debugf("OnRemoteAnswer from peer %s on status %s", conn.config.Key, conn.status.String())
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case conn.remoteAnswerCh <- remoteAuth:
|
case conn.remoteAnswerCh <- answer:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
// connection might not be ready yet to receive so we ignore the message
|
// connection might not be ready yet to receive so we ignore the message
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package peer
|
|||||||
import (
|
import (
|
||||||
"github.com/magiconair/properties/assert"
|
"github.com/magiconair/properties/assert"
|
||||||
"github.com/netbirdio/netbird/client/internal/proxy"
|
"github.com/netbirdio/netbird/client/internal/proxy"
|
||||||
|
nbstatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
"github.com/pion/ice/v2"
|
"github.com/pion/ice/v2"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -16,10 +18,23 @@ var connConf = ConnConfig{
|
|||||||
InterfaceBlackList: nil,
|
InterfaceBlackList: nil,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
ProxyConfig: proxy.Config{},
|
ProxyConfig: proxy.Config{},
|
||||||
|
LocalWgPort: 51820,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewConn_interfaceFilter(t *testing.T) {
|
||||||
|
ignore := []string{iface.WgInterfaceDefault, "tun0", "zt", "ZeroTier", "utun", "wg", "ts",
|
||||||
|
"Tailscale", "tailscale"}
|
||||||
|
|
||||||
|
filter := interfaceFilter(ignore)
|
||||||
|
|
||||||
|
for _, s := range ignore {
|
||||||
|
assert.Equal(t, filter(s), false)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConn_GetKey(t *testing.T) {
|
func TestConn_GetKey(t *testing.T) {
|
||||||
conn, err := NewConn(connConf)
|
conn, err := NewConn(connConf, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -31,7 +46,7 @@ func TestConn_GetKey(t *testing.T) {
|
|||||||
|
|
||||||
func TestConn_OnRemoteOffer(t *testing.T) {
|
func TestConn_OnRemoteOffer(t *testing.T) {
|
||||||
|
|
||||||
conn, err := NewConn(connConf)
|
conn, err := NewConn(connConf, nbstatus.NewRecorder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -45,9 +60,13 @@ func TestConn_OnRemoteOffer(t *testing.T) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
accepted := conn.OnRemoteOffer(IceCredentials{
|
accepted := conn.OnRemoteOffer(OfferAnswer{
|
||||||
|
IceCredentials: IceCredentials{
|
||||||
UFrag: "test",
|
UFrag: "test",
|
||||||
Pwd: "test",
|
Pwd: "test",
|
||||||
|
},
|
||||||
|
WgListenPort: 0,
|
||||||
|
Version: "",
|
||||||
})
|
})
|
||||||
if accepted {
|
if accepted {
|
||||||
wg.Done()
|
wg.Done()
|
||||||
@@ -61,7 +80,7 @@ func TestConn_OnRemoteOffer(t *testing.T) {
|
|||||||
|
|
||||||
func TestConn_OnRemoteAnswer(t *testing.T) {
|
func TestConn_OnRemoteAnswer(t *testing.T) {
|
||||||
|
|
||||||
conn, err := NewConn(connConf)
|
conn, err := NewConn(connConf, nbstatus.NewRecorder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -75,9 +94,13 @@ func TestConn_OnRemoteAnswer(t *testing.T) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
accepted := conn.OnRemoteAnswer(IceCredentials{
|
accepted := conn.OnRemoteAnswer(OfferAnswer{
|
||||||
|
IceCredentials: IceCredentials{
|
||||||
UFrag: "test",
|
UFrag: "test",
|
||||||
Pwd: "test",
|
Pwd: "test",
|
||||||
|
},
|
||||||
|
WgListenPort: 0,
|
||||||
|
Version: "",
|
||||||
})
|
})
|
||||||
if accepted {
|
if accepted {
|
||||||
wg.Done()
|
wg.Done()
|
||||||
@@ -90,7 +113,7 @@ func TestConn_OnRemoteAnswer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
func TestConn_Status(t *testing.T) {
|
func TestConn_Status(t *testing.T) {
|
||||||
|
|
||||||
conn, err := NewConn(connConf)
|
conn, err := NewConn(connConf, nbstatus.NewRecorder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -117,7 +140,7 @@ func TestConn_Status(t *testing.T) {
|
|||||||
|
|
||||||
func TestConn_Close(t *testing.T) {
|
func TestConn_Close(t *testing.T) {
|
||||||
|
|
||||||
conn, err := NewConn(connConf)
|
conn, err := NewConn(connConf, nbstatus.NewRecorder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ type ConnStatus int
|
|||||||
func (s ConnStatus) String() string {
|
func (s ConnStatus) String() string {
|
||||||
switch s {
|
switch s {
|
||||||
case StatusConnecting:
|
case StatusConnecting:
|
||||||
return "StatusConnecting"
|
return "Connecting"
|
||||||
case StatusConnected:
|
case StatusConnected:
|
||||||
return "StatusConnected"
|
return "Connected"
|
||||||
case StatusDisconnected:
|
case StatusDisconnected:
|
||||||
return "StatusDisconnected"
|
return "Disconnected"
|
||||||
default:
|
default:
|
||||||
log.Errorf("unknown status: %d", s)
|
log.Errorf("unknown status: %d", s)
|
||||||
return "INVALID_PEER_CONNECTION_STATUS"
|
return "INVALID_PEER_CONNECTION_STATUS"
|
||||||
@@ -19,7 +19,7 @@ func (s ConnStatus) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StatusConnected = iota
|
StatusConnected ConnStatus = iota
|
||||||
StatusConnecting
|
StatusConnecting
|
||||||
StatusDisconnected
|
StatusDisconnected
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ func TestConnStatus_String(t *testing.T) {
|
|||||||
status ConnStatus
|
status ConnStatus
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{"StatusConnected", StatusConnected, "StatusConnected"},
|
{"StatusConnected", StatusConnected, "Connected"},
|
||||||
{"StatusDisconnected", StatusDisconnected, "StatusDisconnected"},
|
{"StatusDisconnected", StatusDisconnected, "Disconnected"},
|
||||||
{"StatusConnecting", StatusConnecting, "StatusConnecting"},
|
{"StatusConnecting", StatusConnecting, "Connecting"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/netbirdio/netbird/iface"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"net"
|
"net"
|
||||||
)
|
)
|
||||||
@@ -14,10 +13,14 @@ import (
|
|||||||
// In order NoProxy to work, Wireguard port has to be fixed for the time being.
|
// In order NoProxy to work, Wireguard port has to be fixed for the time being.
|
||||||
type NoProxy struct {
|
type NoProxy struct {
|
||||||
config Config
|
config Config
|
||||||
|
// RemoteWgListenPort is a WireGuard port of a remote peer.
|
||||||
|
// It is used instead of the hardcoded 51820 port.
|
||||||
|
RemoteWgListenPort int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNoProxy(config Config) *NoProxy {
|
// NewNoProxy creates a new NoProxy with a provided config and remote peer's WireGuard listen port
|
||||||
return &NoProxy{config: config}
|
func NewNoProxy(config Config, remoteWgPort int) *NoProxy {
|
||||||
|
return &NoProxy{config: config, RemoteWgListenPort: remoteWgPort}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NoProxy) Close() error {
|
func (p *NoProxy) Close() error {
|
||||||
@@ -36,7 +39,7 @@ func (p *NoProxy) Start(remoteConn net.Conn) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
addr.Port = iface.DefaultWgPort
|
addr.Port = p.RemoteWgListenPort
|
||||||
err = p.config.WgInterface.UpdatePeer(p.config.RemoteKey, p.config.AllowedIps, DefaultWgKeepAlive,
|
err = p.config.WgInterface.UpdatePeer(p.config.RemoteKey, p.config.AllowedIps, DefaultWgKeepAlive,
|
||||||
addr, p.config.PreSharedKey)
|
addr, p.config.PreSharedKey)
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
WgListenAddr string
|
WgListenAddr string
|
||||||
RemoteKey string
|
RemoteKey string
|
||||||
WgInterface iface.WGIface
|
WgInterface *iface.WGIface
|
||||||
AllowedIps string
|
AllowedIps string
|
||||||
PreSharedKey *wgtypes.Key
|
PreSharedKey *wgtypes.Key
|
||||||
}
|
}
|
||||||
|
|||||||
285
client/internal/routemanager/client.go
Normal file
285
client/internal/routemanager/client.go
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
type routerPeerStatus struct {
|
||||||
|
connected bool
|
||||||
|
relayed bool
|
||||||
|
direct bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type routesUpdate struct {
|
||||||
|
updateSerial uint64
|
||||||
|
routes []*route.Route
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientNetwork struct {
|
||||||
|
ctx context.Context
|
||||||
|
stop context.CancelFunc
|
||||||
|
statusRecorder *status.Status
|
||||||
|
wgInterface *iface.WGIface
|
||||||
|
routes map[string]*route.Route
|
||||||
|
routeUpdate chan routesUpdate
|
||||||
|
peerStateUpdate chan struct{}
|
||||||
|
routePeersNotifiers map[string]chan struct{}
|
||||||
|
chosenRoute *route.Route
|
||||||
|
network netip.Prefix
|
||||||
|
updateSerial uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClientNetworkWatcher(ctx context.Context, wgInterface *iface.WGIface, statusRecorder *status.Status, network netip.Prefix) *clientNetwork {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
client := &clientNetwork{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
|
wgInterface: wgInterface,
|
||||||
|
routes: make(map[string]*route.Route),
|
||||||
|
routePeersNotifiers: make(map[string]chan struct{}),
|
||||||
|
routeUpdate: make(chan routesUpdate),
|
||||||
|
peerStateUpdate: make(chan struct{}),
|
||||||
|
network: network,
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClientNetworkID(input *route.Route) string {
|
||||||
|
return input.NetID + "-" + input.Network.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) getRouterPeerStatuses() map[string]routerPeerStatus {
|
||||||
|
routePeerStatuses := make(map[string]routerPeerStatus)
|
||||||
|
for _, r := range c.routes {
|
||||||
|
peerStatus, err := c.statusRecorder.GetPeer(r.Peer)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("couldn't fetch peer state: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
routePeerStatuses[r.ID] = routerPeerStatus{
|
||||||
|
connected: peerStatus.ConnStatus == peer.StatusConnected.String(),
|
||||||
|
relayed: peerStatus.Relayed,
|
||||||
|
direct: peerStatus.Direct,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return routePeerStatuses
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[string]routerPeerStatus) string {
|
||||||
|
var chosen string
|
||||||
|
chosenScore := 0
|
||||||
|
|
||||||
|
currID := ""
|
||||||
|
if c.chosenRoute != nil {
|
||||||
|
currID = c.chosenRoute.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range c.routes {
|
||||||
|
tempScore := 0
|
||||||
|
peerStatus, found := routePeerStatuses[r.ID]
|
||||||
|
if !found || !peerStatus.connected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r.Metric < route.MaxMetric {
|
||||||
|
metricDiff := route.MaxMetric - r.Metric
|
||||||
|
tempScore = metricDiff * 10
|
||||||
|
}
|
||||||
|
if !peerStatus.relayed {
|
||||||
|
tempScore++
|
||||||
|
}
|
||||||
|
if !peerStatus.direct {
|
||||||
|
tempScore++
|
||||||
|
}
|
||||||
|
if tempScore > chosenScore || (tempScore == chosenScore && currID == r.ID) {
|
||||||
|
chosen = r.ID
|
||||||
|
chosenScore = tempScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if chosen == "" {
|
||||||
|
var peers []string
|
||||||
|
for _, r := range c.routes {
|
||||||
|
peers = append(peers, r.Peer)
|
||||||
|
}
|
||||||
|
log.Warnf("no route was chosen for network %s because no peers from list %s were connected", c.network, peers)
|
||||||
|
} else if chosen != currID {
|
||||||
|
log.Infof("new chosen route is %s with peer %s with score %d", chosen, c.routes[chosen].Peer, chosenScore)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chosen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) watchPeerStatusChanges(ctx context.Context, peerKey string, peerStateUpdate chan struct{}, closer chan struct{}) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-closer:
|
||||||
|
return
|
||||||
|
case <-c.statusRecorder.GetPeerStateChangeNotifier(peerKey):
|
||||||
|
state, err := c.statusRecorder.GetPeer(peerKey)
|
||||||
|
if err != nil || state.ConnStatus == peer.StatusConnecting.String() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
peerStateUpdate <- struct{}{}
|
||||||
|
log.Debugf("triggered route state update for Peer %s, state: %s", peerKey, state.ConnStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) startPeersStatusChangeWatcher() {
|
||||||
|
for _, r := range c.routes {
|
||||||
|
_, found := c.routePeersNotifiers[r.Peer]
|
||||||
|
if !found {
|
||||||
|
c.routePeersNotifiers[r.Peer] = make(chan struct{})
|
||||||
|
go c.watchPeerStatusChanges(c.ctx, r.Peer, c.peerStateUpdate, c.routePeersNotifiers[r.Peer])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error {
|
||||||
|
state, err := c.statusRecorder.GetPeer(peerKey)
|
||||||
|
if err != nil || state.ConnStatus != peer.StatusConnected.String() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.wgInterface.RemoveAllowedIP(peerKey, c.network.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't remove allowed IP %s removed for peer %s, err: %v",
|
||||||
|
c.network, c.chosenRoute.Peer, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
|
||||||
|
if c.chosenRoute != nil {
|
||||||
|
err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = removeFromRouteTableIfNonSystem(c.network, c.wgInterface.GetAddress().IP.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't remove route %s from system, err: %v",
|
||||||
|
c.network, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
routerPeerStatuses := c.getRouterPeerStatuses()
|
||||||
|
|
||||||
|
chosen := c.getBestRouteFromStatuses(routerPeerStatuses)
|
||||||
|
if chosen == "" {
|
||||||
|
err = c.removeRouteFromPeerAndSystem()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.chosenRoute = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.chosenRoute != nil && c.chosenRoute.ID == chosen {
|
||||||
|
if c.chosenRoute.IsEqual(c.routes[chosen]) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.chosenRoute != nil {
|
||||||
|
err = c.removeRouteFromWireguardPeer(c.chosenRoute.Peer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = addToRouteTableIfNoExists(c.network, c.wgInterface.GetAddress().IP.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("route %s couldn't be added for peer %s, err: %v",
|
||||||
|
c.network.String(), c.wgInterface.GetAddress().IP.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.chosenRoute = c.routes[chosen]
|
||||||
|
err = c.wgInterface.AddAllowedIP(c.chosenRoute.Peer, c.network.String())
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("couldn't add allowed IP %s added for peer %s, err: %v",
|
||||||
|
c.network, c.chosenRoute.Peer, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) {
|
||||||
|
go func() {
|
||||||
|
c.routeUpdate <- update
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) handleUpdate(update routesUpdate) {
|
||||||
|
updateMap := make(map[string]*route.Route)
|
||||||
|
|
||||||
|
for _, r := range update.routes {
|
||||||
|
updateMap[r.ID] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, r := range c.routes {
|
||||||
|
_, found := updateMap[id]
|
||||||
|
if !found {
|
||||||
|
close(c.routePeersNotifiers[r.Peer])
|
||||||
|
delete(c.routePeersNotifiers, r.Peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.routes = updateMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// peersStateAndUpdateWatcher is the main point of reacting on client network routing events.
|
||||||
|
// All the processing related to the client network should be done here. Thread-safe.
|
||||||
|
func (c *clientNetwork) peersStateAndUpdateWatcher() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
log.Debugf("stopping watcher for network %s", c.network)
|
||||||
|
err := c.removeRouteFromPeerAndSystem()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case <-c.peerStateUpdate:
|
||||||
|
err := c.recalculateRouteAndUpdatePeerAndSystem()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
case update := <-c.routeUpdate:
|
||||||
|
if update.updateSerial < c.updateSerial {
|
||||||
|
log.Warnf("received a routes update with smaller serial number, ignoring it")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("received a new client network route update for %s", c.network)
|
||||||
|
|
||||||
|
c.handleUpdate(update)
|
||||||
|
|
||||||
|
c.updateSerial = update.updateSerial
|
||||||
|
|
||||||
|
err := c.recalculateRouteAndUpdatePeerAndSystem()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.startPeersStatusChangeWatcher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
client/internal/routemanager/common_linux_test.go
Normal file
75
client/internal/routemanager/common_linux_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
var insertRuleTestCases = []struct {
|
||||||
|
name string
|
||||||
|
inputPair routerPair
|
||||||
|
ipVersion string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Insert Forwarding IPV4 Rule",
|
||||||
|
inputPair: routerPair{
|
||||||
|
ID: "zxa",
|
||||||
|
source: "100.100.100.1/32",
|
||||||
|
destination: "100.100.200.0/24",
|
||||||
|
masquerade: false,
|
||||||
|
},
|
||||||
|
ipVersion: ipv4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Insert Forwarding And Nat IPV4 Rules",
|
||||||
|
inputPair: routerPair{
|
||||||
|
ID: "zxa",
|
||||||
|
source: "100.100.100.1/32",
|
||||||
|
destination: "100.100.200.0/24",
|
||||||
|
masquerade: true,
|
||||||
|
},
|
||||||
|
ipVersion: ipv4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Insert Forwarding IPV6 Rule",
|
||||||
|
inputPair: routerPair{
|
||||||
|
ID: "zxa",
|
||||||
|
source: "fc00::1/128",
|
||||||
|
destination: "fc12::/64",
|
||||||
|
masquerade: false,
|
||||||
|
},
|
||||||
|
ipVersion: ipv6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Insert Forwarding And Nat IPV6 Rules",
|
||||||
|
inputPair: routerPair{
|
||||||
|
ID: "zxa",
|
||||||
|
source: "fc00::1/128",
|
||||||
|
destination: "fc12::/64",
|
||||||
|
masquerade: true,
|
||||||
|
},
|
||||||
|
ipVersion: ipv6,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeRuleTestCases = []struct {
|
||||||
|
name string
|
||||||
|
inputPair routerPair
|
||||||
|
ipVersion string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Remove Forwarding And Nat IPV4 Rules",
|
||||||
|
inputPair: routerPair{
|
||||||
|
ID: "zxa",
|
||||||
|
source: "100.100.100.1/32",
|
||||||
|
destination: "100.100.200.0/24",
|
||||||
|
masquerade: true,
|
||||||
|
},
|
||||||
|
ipVersion: ipv4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Remove Forwarding And Nat IPV6 Rules",
|
||||||
|
inputPair: routerPair{
|
||||||
|
ID: "zxa",
|
||||||
|
source: "fc00::1/128",
|
||||||
|
destination: "fc12::/64",
|
||||||
|
masquerade: true,
|
||||||
|
},
|
||||||
|
ipVersion: ipv6,
|
||||||
|
},
|
||||||
|
}
|
||||||
12
client/internal/routemanager/firewall.go
Normal file
12
client/internal/routemanager/firewall.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
type firewallManager interface {
|
||||||
|
// RestoreOrCreateContainers restores or creates a firewall container set of rules, tables and default rules
|
||||||
|
RestoreOrCreateContainers() error
|
||||||
|
// InsertRoutingRules inserts a routing firewall rule
|
||||||
|
InsertRoutingRules(pair routerPair) error
|
||||||
|
// RemoveRoutingRules removes a routing firewall rule
|
||||||
|
RemoveRoutingRules(pair routerPair) error
|
||||||
|
// CleanRoutingRules cleans a firewall set of containers
|
||||||
|
CleanRoutingRules()
|
||||||
|
}
|
||||||
67
client/internal/routemanager/firewall_linux.go
Normal file
67
client/internal/routemanager/firewall_linux.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
import "github.com/google/nftables"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ipv6Forwarding = "netbird-rt-ipv6-forwarding"
|
||||||
|
ipv4Forwarding = "netbird-rt-ipv4-forwarding"
|
||||||
|
ipv6Nat = "netbird-rt-ipv6-nat"
|
||||||
|
ipv4Nat = "netbird-rt-ipv4-nat"
|
||||||
|
natFormat = "netbird-nat-%s"
|
||||||
|
forwardingFormat = "netbird-fwd-%s"
|
||||||
|
inNatFormat = "netbird-nat-in-%s"
|
||||||
|
inForwardingFormat = "netbird-fwd-in-%s"
|
||||||
|
ipv6 = "ipv6"
|
||||||
|
ipv4 = "ipv4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func genKey(format string, input string) string {
|
||||||
|
return fmt.Sprintf(format, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFirewall if supported, returns an iptables manager, otherwise returns a nftables manager
|
||||||
|
func NewFirewall(parentCTX context.Context) firewallManager {
|
||||||
|
ctx, cancel := context.WithCancel(parentCTX)
|
||||||
|
|
||||||
|
if isIptablesSupported() {
|
||||||
|
log.Debugf("iptables is supported")
|
||||||
|
ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||||
|
|
||||||
|
return &iptablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
ipv4Client: ipv4Client,
|
||||||
|
ipv6Client: ipv6Client,
|
||||||
|
rules: make(map[string]map[string][]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("iptables is not supported, using nftables")
|
||||||
|
|
||||||
|
manager := &nftablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
conn: &nftables.Conn{},
|
||||||
|
chains: make(map[string]map[string]*nftables.Chain),
|
||||||
|
rules: make(map[string]*nftables.Rule),
|
||||||
|
}
|
||||||
|
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInPair(pair routerPair) routerPair {
|
||||||
|
return routerPair{
|
||||||
|
ID: pair.ID,
|
||||||
|
// invert source/destination
|
||||||
|
source: pair.destination,
|
||||||
|
destination: pair.source,
|
||||||
|
masquerade: pair.masquerade,
|
||||||
|
}
|
||||||
|
}
|
||||||
27
client/internal/routemanager/firewall_nonlinux.go
Normal file
27
client/internal/routemanager/firewall_nonlinux.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//go:build !linux
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package routemanager
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type unimplementedFirewall struct{}
|
||||||
|
|
||||||
|
func (unimplementedFirewall) RestoreOrCreateContainers() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (unimplementedFirewall) InsertRoutingRules(pair routerPair) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (unimplementedFirewall) RemoveRoutingRules(pair routerPair) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (unimplementedFirewall) CleanRoutingRules() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFirewall returns an unimplemented Firewall manager
|
||||||
|
func NewFirewall(parentCtx context.Context) firewallManager {
|
||||||
|
return unimplementedFirewall{}
|
||||||
|
}
|
||||||
436
client/internal/routemanager/iptables_linux.go
Normal file
436
client/internal/routemanager/iptables_linux.go
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net/netip"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isIptablesSupported() bool {
|
||||||
|
_, err4 := exec.LookPath("iptables")
|
||||||
|
_, err6 := exec.LookPath("ip6tables")
|
||||||
|
return err4 == nil && err6 == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// constants needed to manage and create iptable rules
|
||||||
|
const (
|
||||||
|
iptablesFilterTable = "filter"
|
||||||
|
iptablesNatTable = "nat"
|
||||||
|
iptablesForwardChain = "FORWARD"
|
||||||
|
iptablesPostRoutingChain = "POSTROUTING"
|
||||||
|
iptablesRoutingNatChain = "NETBIRD-RT-NAT"
|
||||||
|
iptablesRoutingForwardingChain = "NETBIRD-RT-FWD"
|
||||||
|
routingFinalForwardJump = "ACCEPT"
|
||||||
|
routingFinalNatJump = "MASQUERADE"
|
||||||
|
)
|
||||||
|
|
||||||
|
// some presets for building nftable rules
|
||||||
|
var (
|
||||||
|
iptablesDefaultForwardingRule = []string{"-j", iptablesRoutingForwardingChain, "-m", "comment", "--comment"}
|
||||||
|
iptablesDefaultNetbirdForwardingRule = []string{"-j", "RETURN"}
|
||||||
|
iptablesDefaultNatRule = []string{"-j", iptablesRoutingNatChain, "-m", "comment", "--comment"}
|
||||||
|
iptablesDefaultNetbirdNatRule = []string{"-j", "RETURN"}
|
||||||
|
)
|
||||||
|
|
||||||
|
type iptablesManager struct {
|
||||||
|
ctx context.Context
|
||||||
|
stop context.CancelFunc
|
||||||
|
ipv4Client *iptables.IPTables
|
||||||
|
ipv6Client *iptables.IPTables
|
||||||
|
rules map[string]map[string][]string
|
||||||
|
mux sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanRoutingRules cleans existing iptables resources that we created by the agent
|
||||||
|
func (i *iptablesManager) CleanRoutingRules() {
|
||||||
|
i.mux.Lock()
|
||||||
|
defer i.mux.Unlock()
|
||||||
|
|
||||||
|
err := i.cleanJumpRules()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("flushing tables")
|
||||||
|
errMSGFormat := "iptables: failed cleaning %s chain %s,error: %v"
|
||||||
|
err = i.ipv4Client.ClearAndDeleteChain(iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(errMSGFormat, ipv4, iptablesRoutingForwardingChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.ipv4Client.ClearAndDeleteChain(iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(errMSGFormat, ipv4, iptablesRoutingNatChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.ipv6Client.ClearAndDeleteChain(iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(errMSGFormat, ipv6, iptablesRoutingForwardingChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.ipv6Client.ClearAndDeleteChain(iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(errMSGFormat, ipv6, iptablesRoutingNatChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("done cleaning up iptables rules")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreOrCreateContainers restores existing iptables containers (chains and rules)
|
||||||
|
// if they don't exist, we create them
|
||||||
|
func (i *iptablesManager) RestoreOrCreateContainers() error {
|
||||||
|
i.mux.Lock()
|
||||||
|
defer i.mux.Unlock()
|
||||||
|
|
||||||
|
if i.rules[ipv4][ipv4Forwarding] != nil && i.rules[ipv6][ipv6Forwarding] != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errMSGFormat := "iptables: failed creating %s chain %s,error: %v"
|
||||||
|
|
||||||
|
err := createChain(i.ipv4Client, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv4, iptablesRoutingForwardingChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = createChain(i.ipv4Client, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv4, iptablesRoutingNatChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = createChain(i.ipv6Client, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv6, iptablesRoutingForwardingChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = createChain(i.ipv6Client, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv6, iptablesRoutingNatChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.restoreRules(i.ipv4Client)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables: error while restoring ipv4 rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.restoreRules(i.ipv6Client)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables: error while restoring ipv6 rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.addJumpRules()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables: error while creating jump rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addJumpRules create jump rules to send packets to NetBird chains
|
||||||
|
func (i *iptablesManager) addJumpRules() error {
|
||||||
|
err := i.cleanJumpRules()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rule := append(iptablesDefaultForwardingRule, ipv4Forwarding)
|
||||||
|
err = i.ipv4Client.Insert(iptablesFilterTable, iptablesForwardChain, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.rules[ipv4][ipv4Forwarding] = rule
|
||||||
|
|
||||||
|
rule = append(iptablesDefaultNatRule, ipv4Nat)
|
||||||
|
err = i.ipv4Client.Insert(iptablesNatTable, iptablesPostRoutingChain, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.rules[ipv4][ipv4Nat] = rule
|
||||||
|
|
||||||
|
rule = append(iptablesDefaultForwardingRule, ipv6Forwarding)
|
||||||
|
err = i.ipv6Client.Insert(iptablesFilterTable, iptablesForwardChain, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.rules[ipv6][ipv6Forwarding] = rule
|
||||||
|
|
||||||
|
rule = append(iptablesDefaultNatRule, ipv6Nat)
|
||||||
|
err = i.ipv6Client.Insert(iptablesNatTable, iptablesPostRoutingChain, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.rules[ipv6][ipv6Nat] = rule
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanJumpRules cleans jump rules that was sending packets to NetBird chains
|
||||||
|
func (i *iptablesManager) cleanJumpRules() error {
|
||||||
|
var err error
|
||||||
|
errMSGFormat := "iptables: failed cleaning rule from %s chain %s,err: %v"
|
||||||
|
rule, found := i.rules[ipv4][ipv4Forwarding]
|
||||||
|
if found {
|
||||||
|
log.Debugf("iptables: removing %s rule: %s ", ipv4, ipv4Forwarding)
|
||||||
|
err = i.ipv4Client.DeleteIfExists(iptablesFilterTable, iptablesForwardChain, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv4, iptablesForwardChain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule, found = i.rules[ipv4][ipv4Nat]
|
||||||
|
if found {
|
||||||
|
log.Debugf("iptables: removing %s rule: %s ", ipv4, ipv4Nat)
|
||||||
|
err = i.ipv4Client.DeleteIfExists(iptablesNatTable, iptablesPostRoutingChain, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv4, iptablesPostRoutingChain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule, found = i.rules[ipv6][ipv6Forwarding]
|
||||||
|
if found {
|
||||||
|
log.Debugf("iptables: removing %s rule: %s ", ipv6, ipv6Forwarding)
|
||||||
|
err = i.ipv6Client.DeleteIfExists(iptablesFilterTable, iptablesForwardChain, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv6, iptablesForwardChain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule, found = i.rules[ipv6][ipv6Nat]
|
||||||
|
if found {
|
||||||
|
log.Debugf("iptables: removing %s rule: %s ", ipv6, ipv6Nat)
|
||||||
|
err = i.ipv6Client.DeleteIfExists(iptablesNatTable, iptablesPostRoutingChain, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv6, iptablesPostRoutingChain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func iptablesProtoToString(proto iptables.Protocol) string {
|
||||||
|
if proto == iptables.ProtocolIPv6 {
|
||||||
|
return ipv6
|
||||||
|
}
|
||||||
|
return ipv4
|
||||||
|
}
|
||||||
|
|
||||||
|
// restoreRules restores existing NetBird rules
|
||||||
|
func (i *iptablesManager) restoreRules(iptablesClient *iptables.IPTables) error {
|
||||||
|
ipVersion := iptablesProtoToString(iptablesClient.Proto())
|
||||||
|
|
||||||
|
if i.rules[ipVersion] == nil {
|
||||||
|
i.rules[ipVersion] = make(map[string][]string)
|
||||||
|
}
|
||||||
|
table := iptablesFilterTable
|
||||||
|
for _, chain := range []string{iptablesForwardChain, iptablesRoutingForwardingChain} {
|
||||||
|
rules, err := iptablesClient.List(table, chain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, ruleString := range rules {
|
||||||
|
rule := strings.Fields(ruleString)
|
||||||
|
id := getRuleRouteID(rule)
|
||||||
|
if id != "" {
|
||||||
|
i.rules[ipVersion][id] = rule[2:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table = iptablesNatTable
|
||||||
|
for _, chain := range []string{iptablesPostRoutingChain, iptablesRoutingNatChain} {
|
||||||
|
rules, err := iptablesClient.List(table, chain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, ruleString := range rules {
|
||||||
|
rule := strings.Fields(ruleString)
|
||||||
|
id := getRuleRouteID(rule)
|
||||||
|
if id != "" {
|
||||||
|
i.rules[ipVersion][id] = rule[2:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createChain create NetBird chains
|
||||||
|
func createChain(iptables *iptables.IPTables, table, newChain string) error {
|
||||||
|
chains, err := iptables.ListChains(table)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't get %s %s table chains, error: %v", iptablesProtoToString(iptables.Proto()), table, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldCreateChain := true
|
||||||
|
for _, chain := range chains {
|
||||||
|
if chain == newChain {
|
||||||
|
shouldCreateChain = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCreateChain {
|
||||||
|
err = iptables.NewChain(table, newChain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't create %s chain %s in %s table, error: %v", iptablesProtoToString(iptables.Proto()), newChain, table, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if table == iptablesNatTable {
|
||||||
|
err = iptables.Append(table, newChain, iptablesDefaultNetbirdNatRule...)
|
||||||
|
} else {
|
||||||
|
err = iptables.Append(table, newChain, iptablesDefaultNetbirdForwardingRule...)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't create %s chain %s default rule, error: %v", iptablesProtoToString(iptables.Proto()), newChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// genRuleSpec generates rule specification with comment identifier
|
||||||
|
func genRuleSpec(jump, id, source, destination string) []string {
|
||||||
|
return []string{"-s", source, "-d", destination, "-j", jump, "-m", "comment", "--comment", id}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRuleRouteID returns the rule ID if matches our prefix
|
||||||
|
func getRuleRouteID(rule []string) string {
|
||||||
|
for i, flag := range rule {
|
||||||
|
if flag == "--comment" {
|
||||||
|
id := rule[i+1]
|
||||||
|
if strings.HasPrefix(id, "netbird-") {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertRoutingRules inserts an iptables rule pair to the forwarding chain and if enabled, to the nat chain
|
||||||
|
func (i *iptablesManager) InsertRoutingRules(pair routerPair) error {
|
||||||
|
i.mux.Lock()
|
||||||
|
defer i.mux.Unlock()
|
||||||
|
|
||||||
|
err := i.insertRoutingRule(forwardingFormat, iptablesFilterTable, iptablesRoutingForwardingChain, routingFinalForwardJump, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.insertRoutingRule(inForwardingFormat, iptablesFilterTable, iptablesRoutingForwardingChain, routingFinalForwardJump, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pair.masquerade {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.insertRoutingRule(natFormat, iptablesNatTable, iptablesRoutingNatChain, routingFinalNatJump, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.insertRoutingRule(inNatFormat, iptablesNatTable, iptablesRoutingNatChain, routingFinalNatJump, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertRoutingRule inserts an iptable rule
|
||||||
|
func (i *iptablesManager) insertRoutingRule(keyFormat, table, chain, jump string, pair routerPair) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
prefix := netip.MustParsePrefix(pair.source)
|
||||||
|
ipVersion := ipv4
|
||||||
|
iptablesClient := i.ipv4Client
|
||||||
|
if prefix.Addr().Unmap().Is6() {
|
||||||
|
iptablesClient = i.ipv6Client
|
||||||
|
ipVersion = ipv6
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleKey := genKey(keyFormat, pair.ID)
|
||||||
|
rule := genRuleSpec(jump, ruleKey, pair.source, pair.destination)
|
||||||
|
existingRule, found := i.rules[ipVersion][ruleKey]
|
||||||
|
if found {
|
||||||
|
err = iptablesClient.DeleteIfExists(table, chain, existingRule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables: error while removing existing %s rule for %s: %v", getIptablesRuleType(table), pair.destination, err)
|
||||||
|
}
|
||||||
|
delete(i.rules[ipVersion], ruleKey)
|
||||||
|
}
|
||||||
|
err = iptablesClient.Insert(table, chain, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables: error while adding new %s rule for %s: %v", getIptablesRuleType(table), pair.destination, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.rules[ipVersion][ruleKey] = rule
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRoutingRules removes an iptables rule pair from forwarding and nat chains
|
||||||
|
func (i *iptablesManager) RemoveRoutingRules(pair routerPair) error {
|
||||||
|
i.mux.Lock()
|
||||||
|
defer i.mux.Unlock()
|
||||||
|
|
||||||
|
err := i.removeRoutingRule(forwardingFormat, iptablesFilterTable, iptablesRoutingForwardingChain, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.removeRoutingRule(inForwardingFormat, iptablesFilterTable, iptablesRoutingForwardingChain, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pair.masquerade {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.removeRoutingRule(natFormat, iptablesNatTable, iptablesRoutingNatChain, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.removeRoutingRule(inNatFormat, iptablesNatTable, iptablesRoutingNatChain, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeRoutingRule removes an iptables rule
|
||||||
|
func (i *iptablesManager) removeRoutingRule(keyFormat, table, chain string, pair routerPair) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
prefix := netip.MustParsePrefix(pair.source)
|
||||||
|
ipVersion := ipv4
|
||||||
|
iptablesClient := i.ipv4Client
|
||||||
|
if prefix.Addr().Unmap().Is6() {
|
||||||
|
iptablesClient = i.ipv6Client
|
||||||
|
ipVersion = ipv6
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleKey := genKey(keyFormat, pair.ID)
|
||||||
|
existingRule, found := i.rules[ipVersion][ruleKey]
|
||||||
|
if found {
|
||||||
|
err = iptablesClient.DeleteIfExists(table, chain, existingRule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables: error while removing existing %s rule for %s: %v", getIptablesRuleType(table), pair.destination, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(i.rules[ipVersion], ruleKey)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIptablesRuleType(table string) string {
|
||||||
|
ruleType := "forwarding"
|
||||||
|
if table == iptablesNatTable {
|
||||||
|
ruleType = "nat"
|
||||||
|
}
|
||||||
|
return ruleType
|
||||||
|
}
|
||||||
300
client/internal/routemanager/iptables_linux_test.go
Normal file
300
client/internal/routemanager/iptables_linux_test.go
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
|
||||||
|
|
||||||
|
if !isIptablesSupported() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||||
|
|
||||||
|
manager := &iptablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
ipv4Client: ipv4Client,
|
||||||
|
ipv6Client: ipv6Client,
|
||||||
|
rules: make(map[string]map[string][]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
err := manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
require.Len(t, manager.rules, 2, "should have created maps for ipv4 and ipv6")
|
||||||
|
|
||||||
|
require.Len(t, manager.rules[ipv4], 2, "should have created minimal rules for ipv4")
|
||||||
|
|
||||||
|
exists, err := ipv4Client.Exists(iptablesFilterTable, iptablesForwardChain, manager.rules[ipv4][ipv4Forwarding]...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", ipv4, iptablesFilterTable, iptablesForwardChain)
|
||||||
|
require.True(t, exists, "forwarding rule should exist")
|
||||||
|
|
||||||
|
exists, err = ipv4Client.Exists(iptablesNatTable, iptablesPostRoutingChain, manager.rules[ipv4][ipv4Nat]...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", ipv4, iptablesNatTable, iptablesPostRoutingChain)
|
||||||
|
require.True(t, exists, "postrouting rule should exist")
|
||||||
|
|
||||||
|
require.Len(t, manager.rules[ipv6], 2, "should have created minimal rules for ipv6")
|
||||||
|
|
||||||
|
exists, err = ipv6Client.Exists(iptablesFilterTable, iptablesForwardChain, manager.rules[ipv6][ipv6Forwarding]...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", ipv6, iptablesFilterTable, iptablesForwardChain)
|
||||||
|
require.True(t, exists, "forwarding rule should exist")
|
||||||
|
|
||||||
|
exists, err = ipv6Client.Exists(iptablesNatTable, iptablesPostRoutingChain, manager.rules[ipv6][ipv6Nat]...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", ipv6, iptablesNatTable, iptablesPostRoutingChain)
|
||||||
|
require.True(t, exists, "postrouting rule should exist")
|
||||||
|
|
||||||
|
pair := routerPair{
|
||||||
|
ID: "abc",
|
||||||
|
source: "100.100.100.1/32",
|
||||||
|
destination: "100.100.100.0/24",
|
||||||
|
masquerade: true,
|
||||||
|
}
|
||||||
|
forward4RuleKey := genKey(forwardingFormat, pair.ID)
|
||||||
|
forward4Rule := genRuleSpec(routingFinalForwardJump, forward4RuleKey, pair.source, pair.destination)
|
||||||
|
|
||||||
|
err = ipv4Client.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forward4Rule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
nat4RuleKey := genKey(natFormat, pair.ID)
|
||||||
|
nat4Rule := genRuleSpec(routingFinalNatJump, nat4RuleKey, pair.source, pair.destination)
|
||||||
|
|
||||||
|
err = ipv4Client.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, nat4Rule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
pair = routerPair{
|
||||||
|
ID: "abc",
|
||||||
|
source: "fc00::1/128",
|
||||||
|
destination: "fc11::/64",
|
||||||
|
masquerade: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
forward6RuleKey := genKey(forwardingFormat, pair.ID)
|
||||||
|
forward6Rule := genRuleSpec(routingFinalForwardJump, forward6RuleKey, pair.source, pair.destination)
|
||||||
|
|
||||||
|
err = ipv6Client.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forward6Rule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
nat6RuleKey := genKey(natFormat, pair.ID)
|
||||||
|
nat6Rule := genRuleSpec(routingFinalNatJump, nat6RuleKey, pair.source, pair.destination)
|
||||||
|
|
||||||
|
err = ipv6Client.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, nat6Rule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
delete(manager.rules, ipv4)
|
||||||
|
delete(manager.rules, ipv6)
|
||||||
|
|
||||||
|
err = manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
require.Len(t, manager.rules[ipv4], 4, "should have restored all rules for ipv4")
|
||||||
|
|
||||||
|
foundRule, found := manager.rules[ipv4][forward4RuleKey]
|
||||||
|
require.True(t, found, "forwarding rule should exist in the map")
|
||||||
|
require.Equal(t, forward4Rule[:4], foundRule[:4], "stored forwarding rule should match")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[ipv4][nat4RuleKey]
|
||||||
|
require.True(t, found, "nat rule should exist in the map")
|
||||||
|
require.Equal(t, nat4Rule[:4], foundRule[:4], "stored nat rule should match")
|
||||||
|
|
||||||
|
require.Len(t, manager.rules[ipv6], 4, "should have restored all rules for ipv6")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[ipv6][forward6RuleKey]
|
||||||
|
require.True(t, found, "forwarding rule should exist in the map")
|
||||||
|
require.Equal(t, forward6Rule[:4], foundRule[:4], "stored forward rule should match")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[ipv6][nat6RuleKey]
|
||||||
|
require.True(t, found, "nat rule should exist in the map")
|
||||||
|
require.Equal(t, nat6Rule[:4], foundRule[:4], "stored nat rule should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIptablesManager_InsertRoutingRules(t *testing.T) {
|
||||||
|
|
||||||
|
if !isIptablesSupported() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range insertRuleTestCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||||
|
iptablesClient := ipv4Client
|
||||||
|
if testCase.ipVersion == ipv6 {
|
||||||
|
iptablesClient = ipv6Client
|
||||||
|
}
|
||||||
|
|
||||||
|
manager := &iptablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
ipv4Client: ipv4Client,
|
||||||
|
ipv6Client: ipv6Client,
|
||||||
|
rules: make(map[string]map[string][]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
err := manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
err = manager.InsertRoutingRules(testCase.inputPair)
|
||||||
|
require.NoError(t, err, "forwarding pair should be inserted")
|
||||||
|
|
||||||
|
forwardRuleKey := genKey(forwardingFormat, testCase.inputPair.ID)
|
||||||
|
forwardRule := genRuleSpec(routingFinalForwardJump, forwardRuleKey, testCase.inputPair.source, testCase.inputPair.destination)
|
||||||
|
|
||||||
|
exists, err := iptablesClient.Exists(iptablesFilterTable, iptablesRoutingForwardingChain, forwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
require.True(t, exists, "forwarding rule should exist")
|
||||||
|
|
||||||
|
foundRule, found := manager.rules[testCase.ipVersion][forwardRuleKey]
|
||||||
|
require.True(t, found, "forwarding rule should exist in the manager map")
|
||||||
|
require.Equal(t, forwardRule[:4], foundRule[:4], "stored forwarding rule should match")
|
||||||
|
|
||||||
|
inForwardRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID)
|
||||||
|
inForwardRule := genRuleSpec(routingFinalForwardJump, inForwardRuleKey, getInPair(testCase.inputPair).source, getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesFilterTable, iptablesRoutingForwardingChain, inForwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
require.True(t, exists, "income forwarding rule should exist")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[testCase.ipVersion][inForwardRuleKey]
|
||||||
|
require.True(t, found, "income forwarding rule should exist in the manager map")
|
||||||
|
require.Equal(t, inForwardRule[:4], foundRule[:4], "stored income forwarding rule should match")
|
||||||
|
|
||||||
|
natRuleKey := genKey(natFormat, testCase.inputPair.ID)
|
||||||
|
natRule := genRuleSpec(routingFinalNatJump, natRuleKey, testCase.inputPair.source, testCase.inputPair.destination)
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesNatTable, iptablesRoutingNatChain, natRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if testCase.inputPair.masquerade {
|
||||||
|
require.True(t, exists, "nat rule should be created")
|
||||||
|
foundNatRule, foundNat := manager.rules[testCase.ipVersion][natRuleKey]
|
||||||
|
require.True(t, foundNat, "nat rule should exist in the map")
|
||||||
|
require.Equal(t, natRule[:4], foundNatRule[:4], "stored nat rule should match")
|
||||||
|
} else {
|
||||||
|
require.False(t, exists, "nat rule should not be created")
|
||||||
|
_, foundNat := manager.rules[testCase.ipVersion][natRuleKey]
|
||||||
|
require.False(t, foundNat, "nat rule should not exist in the map")
|
||||||
|
}
|
||||||
|
|
||||||
|
inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID)
|
||||||
|
inNatRule := genRuleSpec(routingFinalNatJump, inNatRuleKey, getInPair(testCase.inputPair).source, getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesNatTable, iptablesRoutingNatChain, inNatRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if testCase.inputPair.masquerade {
|
||||||
|
require.True(t, exists, "income nat rule should be created")
|
||||||
|
foundNatRule, foundNat := manager.rules[testCase.ipVersion][inNatRuleKey]
|
||||||
|
require.True(t, foundNat, "income nat rule should exist in the map")
|
||||||
|
require.Equal(t, inNatRule[:4], foundNatRule[:4], "stored income nat rule should match")
|
||||||
|
} else {
|
||||||
|
require.False(t, exists, "nat rule should not be created")
|
||||||
|
_, foundNat := manager.rules[testCase.ipVersion][inNatRuleKey]
|
||||||
|
require.False(t, foundNat, "income nat rule should not exist in the map")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIptablesManager_RemoveRoutingRules(t *testing.T) {
|
||||||
|
|
||||||
|
if !isIptablesSupported() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range removeRuleTestCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||||
|
iptablesClient := ipv4Client
|
||||||
|
if testCase.ipVersion == ipv6 {
|
||||||
|
iptablesClient = ipv6Client
|
||||||
|
}
|
||||||
|
|
||||||
|
manager := &iptablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
ipv4Client: ipv4Client,
|
||||||
|
ipv6Client: ipv6Client,
|
||||||
|
rules: make(map[string]map[string][]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
err := manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
forwardRuleKey := genKey(forwardingFormat, testCase.inputPair.ID)
|
||||||
|
forwardRule := genRuleSpec(routingFinalForwardJump, forwardRuleKey, testCase.inputPair.source, testCase.inputPair.destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forwardRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
inForwardRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID)
|
||||||
|
inForwardRule := genRuleSpec(routingFinalForwardJump, inForwardRuleKey, getInPair(testCase.inputPair).source, getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, inForwardRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
natRuleKey := genKey(natFormat, testCase.inputPair.ID)
|
||||||
|
natRule := genRuleSpec(routingFinalNatJump, natRuleKey, testCase.inputPair.source, testCase.inputPair.destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, natRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID)
|
||||||
|
inNatRule := genRuleSpec(routingFinalNatJump, inNatRuleKey, getInPair(testCase.inputPair).source, getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, inNatRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
delete(manager.rules, ipv4)
|
||||||
|
delete(manager.rules, ipv6)
|
||||||
|
|
||||||
|
err = manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
err = manager.RemoveRoutingRules(testCase.inputPair)
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
exists, err := iptablesClient.Exists(iptablesFilterTable, iptablesRoutingForwardingChain, forwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
require.False(t, exists, "forwarding rule should not exist")
|
||||||
|
|
||||||
|
_, found := manager.rules[testCase.ipVersion][forwardRuleKey]
|
||||||
|
require.False(t, found, "forwarding rule should exist in the manager map")
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesFilterTable, iptablesRoutingForwardingChain, inForwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
require.False(t, exists, "income forwarding rule should not exist")
|
||||||
|
|
||||||
|
_, found = manager.rules[testCase.ipVersion][inForwardRuleKey]
|
||||||
|
require.False(t, found, "income forwarding rule should exist in the manager map")
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesNatTable, iptablesRoutingNatChain, natRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
require.False(t, exists, "nat rule should not exist")
|
||||||
|
|
||||||
|
_, found = manager.rules[testCase.ipVersion][natRuleKey]
|
||||||
|
require.False(t, found, "nat rule should exist in the manager map")
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesNatTable, iptablesRoutingNatChain, inNatRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
require.False(t, exists, "income nat rule should not exist")
|
||||||
|
|
||||||
|
_, found = manager.rules[testCase.ipVersion][inNatRuleKey]
|
||||||
|
require.False(t, found, "income nat rule should exist in the manager map")
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
181
client/internal/routemanager/manager.go
Normal file
181
client/internal/routemanager/manager.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager is a route manager interface
|
||||||
|
type Manager interface {
|
||||||
|
UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error
|
||||||
|
Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultManager is the default instance of a route manager
|
||||||
|
type DefaultManager struct {
|
||||||
|
ctx context.Context
|
||||||
|
stop context.CancelFunc
|
||||||
|
mux sync.Mutex
|
||||||
|
clientNetworks map[string]*clientNetwork
|
||||||
|
serverRoutes map[string]*route.Route
|
||||||
|
serverRouter *serverRouter
|
||||||
|
statusRecorder *status.Status
|
||||||
|
wgInterface *iface.WGIface
|
||||||
|
pubKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager returns a new route manager
|
||||||
|
func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, statusRecorder *status.Status) *DefaultManager {
|
||||||
|
mCTX, cancel := context.WithCancel(ctx)
|
||||||
|
return &DefaultManager{
|
||||||
|
ctx: mCTX,
|
||||||
|
stop: cancel,
|
||||||
|
clientNetworks: make(map[string]*clientNetwork),
|
||||||
|
serverRoutes: make(map[string]*route.Route),
|
||||||
|
serverRouter: &serverRouter{
|
||||||
|
routes: make(map[string]*route.Route),
|
||||||
|
netForwardHistoryEnabled: isNetForwardHistoryEnabled(),
|
||||||
|
firewall: NewFirewall(ctx),
|
||||||
|
},
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
|
wgInterface: wgInterface,
|
||||||
|
pubKey: pubKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the manager watchers and clean firewall rules
|
||||||
|
func (m *DefaultManager) Stop() {
|
||||||
|
m.stop()
|
||||||
|
m.serverRouter.firewall.CleanRoutingRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks map[string][]*route.Route) {
|
||||||
|
// removing routes that do not exist as per the update from the Management service.
|
||||||
|
for id, client := range m.clientNetworks {
|
||||||
|
_, found := networks[id]
|
||||||
|
if !found {
|
||||||
|
log.Debugf("stopping client network watcher, %s", id)
|
||||||
|
client.stop()
|
||||||
|
delete(m.clientNetworks, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, routes := range networks {
|
||||||
|
clientNetworkWatcher, found := m.clientNetworks[id]
|
||||||
|
if !found {
|
||||||
|
clientNetworkWatcher = newClientNetworkWatcher(m.ctx, m.wgInterface, m.statusRecorder, routes[0].Network)
|
||||||
|
m.clientNetworks[id] = clientNetworkWatcher
|
||||||
|
go clientNetworkWatcher.peersStateAndUpdateWatcher()
|
||||||
|
}
|
||||||
|
update := routesUpdate{
|
||||||
|
updateSerial: updateSerial,
|
||||||
|
routes: routes,
|
||||||
|
}
|
||||||
|
|
||||||
|
clientNetworkWatcher.sendUpdateToClientNetworkWatcher(update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DefaultManager) updateServerRoutes(routesMap map[string]*route.Route) error {
|
||||||
|
serverRoutesToRemove := make([]string, 0)
|
||||||
|
|
||||||
|
if len(routesMap) > 0 {
|
||||||
|
err := m.serverRouter.firewall.RestoreOrCreateContainers()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't initialize firewall containers, got err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for routeID := range m.serverRoutes {
|
||||||
|
update, found := routesMap[routeID]
|
||||||
|
if !found || !update.IsEqual(m.serverRoutes[routeID]) {
|
||||||
|
serverRoutesToRemove = append(serverRoutesToRemove, routeID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, routeID := range serverRoutesToRemove {
|
||||||
|
oldRoute := m.serverRoutes[routeID]
|
||||||
|
err := m.removeFromServerNetwork(oldRoute)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to remove route id: %s, network %s, from server, got: %v",
|
||||||
|
oldRoute.ID, oldRoute.Network, err)
|
||||||
|
}
|
||||||
|
delete(m.serverRoutes, routeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, newRoute := range routesMap {
|
||||||
|
_, found := m.serverRoutes[id]
|
||||||
|
if found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := m.addToServerNetwork(newRoute)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to add route %s from server, got: %v", newRoute.ID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.serverRoutes[id] = newRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.serverRoutes) > 0 {
|
||||||
|
err := enableIPForwarding()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRoutes compares received routes with existing routes and remove, update or add them to the client and server maps
|
||||||
|
func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error {
|
||||||
|
select {
|
||||||
|
case <-m.ctx.Done():
|
||||||
|
log.Infof("not updating routes as context is closed")
|
||||||
|
return m.ctx.Err()
|
||||||
|
default:
|
||||||
|
m.mux.Lock()
|
||||||
|
defer m.mux.Unlock()
|
||||||
|
|
||||||
|
newClientRoutesIDMap := make(map[string][]*route.Route)
|
||||||
|
newServerRoutesMap := make(map[string]*route.Route)
|
||||||
|
|
||||||
|
for _, newRoute := range newRoutes {
|
||||||
|
// only linux is supported for now
|
||||||
|
if newRoute.Peer == m.pubKey {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
log.Warnf("received a route to manage, but agent doesn't support router mode on %s OS", runtime.GOOS)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newServerRoutesMap[newRoute.ID] = newRoute
|
||||||
|
} else {
|
||||||
|
// if prefix is too small, lets assume is a possible default route which is not yet supported
|
||||||
|
// we skip this route management
|
||||||
|
if newRoute.Network.Bits() < 7 {
|
||||||
|
log.Errorf("this agent version: %s, doesn't support default routes, received %s, skiping this route",
|
||||||
|
system.NetbirdVersion(), newRoute.Network)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clientNetworkID := getClientNetworkID(newRoute)
|
||||||
|
newClientRoutesIDMap[clientNetworkID] = append(newClientRoutesIDMap[clientNetworkID], newRoute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.updateClientNetworks(updateSerial, newClientRoutesIDMap)
|
||||||
|
|
||||||
|
err := m.updateServerRoutes(newServerRoutesMap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
370
client/internal/routemanager/manager_test.go
Normal file
370
client/internal/routemanager/manager_test.go
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// send 5 routes, one for server and 4 for clients, one normal and 2 HA and one small
|
||||||
|
// if linux host, should have one for server in map
|
||||||
|
// we should have 2 client manager
|
||||||
|
// 2 ranges in our routing table
|
||||||
|
|
||||||
|
const localPeerKey = "local"
|
||||||
|
const remotePeerKey1 = "remote1"
|
||||||
|
const remotePeerKey2 = "remote1"
|
||||||
|
|
||||||
|
func TestManagerUpdateRoutes(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
inputInitRoutes []*route.Route
|
||||||
|
inputRoutes []*route.Route
|
||||||
|
inputSerial uint64
|
||||||
|
shouldCheckServerRoutes bool
|
||||||
|
serverRoutesExpected int
|
||||||
|
clientNetworkWatchersExpected int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Should create 2 client networks",
|
||||||
|
inputInitRoutes: []*route.Route{},
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.8.8/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
clientNetworkWatchersExpected: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Create 2 Server Routes",
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: localPeerKey,
|
||||||
|
Network: netip.MustParsePrefix("100.64.252.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: localPeerKey,
|
||||||
|
Network: netip.MustParsePrefix("8.8.8.9/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
shouldCheckServerRoutes: runtime.GOOS == "linux",
|
||||||
|
serverRoutesExpected: 2,
|
||||||
|
clientNetworkWatchersExpected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Create 1 Route For Client And Server",
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: localPeerKey,
|
||||||
|
Network: netip.MustParsePrefix("100.64.30.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.9.9/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
shouldCheckServerRoutes: runtime.GOOS == "linux",
|
||||||
|
serverRoutesExpected: 1,
|
||||||
|
clientNetworkWatchersExpected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Create 1 HA Route and 1 Standalone",
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.20.0/24"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey2,
|
||||||
|
Network: netip.MustParsePrefix("8.8.20.0/24"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "c",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.9.9/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
clientNetworkWatchersExpected: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No Small Client Route Should Be Added",
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
clientNetworkWatchersExpected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No Server Routes Should Be Added To Non Linux",
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: localPeerKey,
|
||||||
|
Network: netip.MustParsePrefix("1.2.3.4/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
shouldCheckServerRoutes: runtime.GOOS != "linux",
|
||||||
|
serverRoutesExpected: 0,
|
||||||
|
clientNetworkWatchersExpected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Remove 1 Client Route",
|
||||||
|
inputInitRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.8.8/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
clientNetworkWatchersExpected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update Route to HA",
|
||||||
|
inputInitRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.8.8/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey2,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
clientNetworkWatchersExpected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Remove Client Routes",
|
||||||
|
inputInitRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.8.8/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputRoutes: []*route.Route{},
|
||||||
|
inputSerial: 1,
|
||||||
|
clientNetworkWatchersExpected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Remove All Routes",
|
||||||
|
inputInitRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: localPeerKey,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.8.8/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputRoutes: []*route.Route{},
|
||||||
|
inputSerial: 1,
|
||||||
|
shouldCheckServerRoutes: true,
|
||||||
|
serverRoutesExpected: 0,
|
||||||
|
clientNetworkWatchersExpected: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun43%d", n), "100.65.65.2/24", iface.DefaultMTU)
|
||||||
|
require.NoError(t, err, "should create testing WGIface interface")
|
||||||
|
defer wgInterface.Close()
|
||||||
|
|
||||||
|
err = wgInterface.Create()
|
||||||
|
require.NoError(t, err, "should create testing wireguard interface")
|
||||||
|
|
||||||
|
statusRecorder := status.NewRecorder()
|
||||||
|
ctx := context.TODO()
|
||||||
|
routeManager := NewManager(ctx, localPeerKey, wgInterface, statusRecorder)
|
||||||
|
defer routeManager.Stop()
|
||||||
|
|
||||||
|
if len(testCase.inputInitRoutes) > 0 {
|
||||||
|
err = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes)
|
||||||
|
require.NoError(t, err, "should update routes with init routes")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), testCase.inputRoutes)
|
||||||
|
require.NoError(t, err, "should update routes")
|
||||||
|
|
||||||
|
require.Len(t, routeManager.clientNetworks, testCase.clientNetworkWatchersExpected, "client networks size should match")
|
||||||
|
|
||||||
|
if testCase.shouldCheckServerRoutes {
|
||||||
|
require.Len(t, routeManager.serverRoutes, testCase.serverRoutesExpected, "server networks size should match")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
27
client/internal/routemanager/mock.go
Normal file
27
client/internal/routemanager/mock.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockManager is the mock instance of a route manager
|
||||||
|
type MockManager struct {
|
||||||
|
UpdateRoutesFunc func(updateSerial uint64, newRoutes []*route.Route) error
|
||||||
|
StopFunc func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRoutes mock implementation of UpdateRoutes from Manager interface
|
||||||
|
func (m *MockManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error {
|
||||||
|
if m.UpdateRoutesFunc != nil {
|
||||||
|
return m.UpdateRoutesFunc(updateSerial, newRoutes)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("method UpdateRoutes is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop mock implementation of Stop from Manager interface
|
||||||
|
func (m *MockManager) Stop() {
|
||||||
|
if m.StopFunc != nil {
|
||||||
|
m.StopFunc()
|
||||||
|
}
|
||||||
|
}
|
||||||
432
client/internal/routemanager/nftables_linux.go
Normal file
432
client/internal/routemanager/nftables_linux.go
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/nftables/binaryutil"
|
||||||
|
"github.com/google/nftables/expr"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
import "github.com/google/nftables"
|
||||||
|
|
||||||
|
const (
|
||||||
|
nftablesTable = "netbird-rt"
|
||||||
|
nftablesRoutingForwardingChain = "netbird-rt-fwd"
|
||||||
|
nftablesRoutingNatChain = "netbird-rt-nat"
|
||||||
|
)
|
||||||
|
|
||||||
|
// constants needed to create nftable rules
|
||||||
|
const (
|
||||||
|
ipv4Len = 4
|
||||||
|
ipv4SrcOffset = 12
|
||||||
|
ipv4DestOffset = 16
|
||||||
|
ipv6Len = 16
|
||||||
|
ipv6SrcOffset = 8
|
||||||
|
ipv6DestOffset = 24
|
||||||
|
exprDirectionSource = "source"
|
||||||
|
exprDirectionDestination = "destination"
|
||||||
|
)
|
||||||
|
|
||||||
|
// some presets for building nftable rules
|
||||||
|
var (
|
||||||
|
zeroXor = binaryutil.NativeEndian.PutUint32(0)
|
||||||
|
|
||||||
|
zeroXor6 = append(binaryutil.NativeEndian.PutUint64(0), binaryutil.NativeEndian.PutUint64(0)...)
|
||||||
|
|
||||||
|
exprAllowRelatedEstablished = []expr.Any{
|
||||||
|
&expr.Ct{
|
||||||
|
Register: 1,
|
||||||
|
SourceRegister: false,
|
||||||
|
Key: 0,
|
||||||
|
},
|
||||||
|
&expr.Bitwise{
|
||||||
|
DestRegister: 1,
|
||||||
|
SourceRegister: 1,
|
||||||
|
Len: 4,
|
||||||
|
Mask: []uint8{0x6, 0x0, 0x0, 0x0},
|
||||||
|
Xor: zeroXor,
|
||||||
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Register: 1,
|
||||||
|
Data: binaryutil.NativeEndian.PutUint32(0),
|
||||||
|
},
|
||||||
|
&expr.Counter{},
|
||||||
|
&expr.Verdict{
|
||||||
|
Kind: expr.VerdictAccept,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
exprCounterAccept = []expr.Any{
|
||||||
|
&expr.Counter{},
|
||||||
|
&expr.Verdict{
|
||||||
|
Kind: expr.VerdictAccept,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type nftablesManager struct {
|
||||||
|
ctx context.Context
|
||||||
|
stop context.CancelFunc
|
||||||
|
conn *nftables.Conn
|
||||||
|
tableIPv4 *nftables.Table
|
||||||
|
tableIPv6 *nftables.Table
|
||||||
|
chains map[string]map[string]*nftables.Chain
|
||||||
|
rules map[string]*nftables.Rule
|
||||||
|
mux sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanRoutingRules cleans existing nftables rules from the system
|
||||||
|
func (n *nftablesManager) CleanRoutingRules() {
|
||||||
|
n.mux.Lock()
|
||||||
|
defer n.mux.Unlock()
|
||||||
|
log.Debug("flushing tables")
|
||||||
|
if n.tableIPv4 != nil && n.tableIPv6 != nil {
|
||||||
|
n.conn.FlushTable(n.tableIPv6)
|
||||||
|
n.conn.FlushTable(n.tableIPv4)
|
||||||
|
}
|
||||||
|
log.Debugf("flushing tables result in: %v error", n.conn.Flush())
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreOrCreateContainers restores existing nftables containers (tables and chains)
|
||||||
|
// if they don't exist, we create them
|
||||||
|
func (n *nftablesManager) RestoreOrCreateContainers() error {
|
||||||
|
n.mux.Lock()
|
||||||
|
defer n.mux.Unlock()
|
||||||
|
|
||||||
|
if n.tableIPv6 != nil && n.tableIPv4 != nil {
|
||||||
|
log.Debugf("nftables: containers already restored, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tables, err := n.conn.ListTables()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to list tables: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
if table.Name == nftablesTable {
|
||||||
|
if table.Family == nftables.TableFamilyIPv4 {
|
||||||
|
n.tableIPv4 = table
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n.tableIPv6 = table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.tableIPv4 == nil {
|
||||||
|
n.tableIPv4 = n.conn.AddTable(&nftables.Table{
|
||||||
|
Name: nftablesTable,
|
||||||
|
Family: nftables.TableFamilyIPv4,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.tableIPv6 == nil {
|
||||||
|
n.tableIPv6 = n.conn.AddTable(&nftables.Table{
|
||||||
|
Name: nftablesTable,
|
||||||
|
Family: nftables.TableFamilyIPv6,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
chains, err := n.conn.ListChains()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to list chains: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n.chains[ipv4] = make(map[string]*nftables.Chain)
|
||||||
|
n.chains[ipv6] = make(map[string]*nftables.Chain)
|
||||||
|
|
||||||
|
for _, chain := range chains {
|
||||||
|
switch {
|
||||||
|
case chain.Table.Name == nftablesTable && chain.Table.Family == nftables.TableFamilyIPv4:
|
||||||
|
n.chains[ipv4][chain.Name] = chain
|
||||||
|
case chain.Table.Name == nftablesTable && chain.Table.Family == nftables.TableFamilyIPv6:
|
||||||
|
n.chains[ipv6][chain.Name] = chain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := n.chains[ipv4][nftablesRoutingForwardingChain]; !found {
|
||||||
|
n.chains[ipv4][nftablesRoutingForwardingChain] = n.conn.AddChain(&nftables.Chain{
|
||||||
|
Name: nftablesRoutingForwardingChain,
|
||||||
|
Table: n.tableIPv4,
|
||||||
|
Hooknum: nftables.ChainHookForward,
|
||||||
|
Priority: nftables.ChainPriorityNATDest + 1,
|
||||||
|
Type: nftables.ChainTypeFilter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := n.chains[ipv4][nftablesRoutingNatChain]; !found {
|
||||||
|
n.chains[ipv4][nftablesRoutingNatChain] = n.conn.AddChain(&nftables.Chain{
|
||||||
|
Name: nftablesRoutingNatChain,
|
||||||
|
Table: n.tableIPv4,
|
||||||
|
Hooknum: nftables.ChainHookPostrouting,
|
||||||
|
Priority: nftables.ChainPriorityNATSource - 1,
|
||||||
|
Type: nftables.ChainTypeNAT,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := n.chains[ipv6][nftablesRoutingForwardingChain]; !found {
|
||||||
|
n.chains[ipv6][nftablesRoutingForwardingChain] = n.conn.AddChain(&nftables.Chain{
|
||||||
|
Name: nftablesRoutingForwardingChain,
|
||||||
|
Table: n.tableIPv6,
|
||||||
|
Hooknum: nftables.ChainHookForward,
|
||||||
|
Priority: nftables.ChainPriorityNATDest + 1,
|
||||||
|
Type: nftables.ChainTypeFilter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := n.chains[ipv6][nftablesRoutingNatChain]; !found {
|
||||||
|
n.chains[ipv6][nftablesRoutingNatChain] = n.conn.AddChain(&nftables.Chain{
|
||||||
|
Name: nftablesRoutingNatChain,
|
||||||
|
Table: n.tableIPv6,
|
||||||
|
Hooknum: nftables.ChainHookPostrouting,
|
||||||
|
Priority: nftables.ChainPriorityNATSource - 1,
|
||||||
|
Type: nftables.ChainTypeNAT,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.refreshRulesMap()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n.checkOrCreateDefaultForwardingRules()
|
||||||
|
err = n.conn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to initialize table: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshRulesMap refreshes the rule map with the latest rules. this is useful to avoid
|
||||||
|
// duplicates and to get missing attributes that we don't have when adding new rules
|
||||||
|
func (n *nftablesManager) refreshRulesMap() error {
|
||||||
|
for _, registeredChains := range n.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := n.conn.GetRules(chain.Table, chain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to list rules: %v", err)
|
||||||
|
}
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 {
|
||||||
|
n.rules[string(rule.UserData)] = rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOrCreateDefaultForwardingRules checks if the default forwarding rules are enabled
|
||||||
|
func (n *nftablesManager) checkOrCreateDefaultForwardingRules() {
|
||||||
|
_, foundIPv4 := n.rules[ipv4Forwarding]
|
||||||
|
if !foundIPv4 {
|
||||||
|
n.rules[ipv4Forwarding] = n.conn.AddRule(&nftables.Rule{
|
||||||
|
Table: n.tableIPv4,
|
||||||
|
Chain: n.chains[ipv4][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: exprAllowRelatedEstablished,
|
||||||
|
UserData: []byte(ipv4Forwarding),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, foundIPv6 := n.rules[ipv6Forwarding]
|
||||||
|
if !foundIPv6 {
|
||||||
|
n.rules[ipv6Forwarding] = n.conn.AddRule(&nftables.Rule{
|
||||||
|
Table: n.tableIPv6,
|
||||||
|
Chain: n.chains[ipv6][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: exprAllowRelatedEstablished,
|
||||||
|
UserData: []byte(ipv6Forwarding),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertRoutingRules inserts a nftable rule pair to the forwarding chain and if enabled, to the nat chain
|
||||||
|
func (n *nftablesManager) InsertRoutingRules(pair routerPair) error {
|
||||||
|
n.mux.Lock()
|
||||||
|
defer n.mux.Unlock()
|
||||||
|
|
||||||
|
err := n.refreshRulesMap()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.insertRoutingRule(forwardingFormat, nftablesRoutingForwardingChain, pair, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = n.insertRoutingRule(inForwardingFormat, nftablesRoutingForwardingChain, getInPair(pair), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pair.masquerade {
|
||||||
|
err = n.insertRoutingRule(natFormat, nftablesRoutingNatChain, pair, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = n.insertRoutingRule(inNatFormat, nftablesRoutingNatChain, getInPair(pair), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.conn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to insert rules for %s: %v", pair.destination, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertRoutingRule inserts a nftable rule to the conn client flush queue
|
||||||
|
func (n *nftablesManager) insertRoutingRule(format, chain string, pair routerPair, isNat bool) error {
|
||||||
|
|
||||||
|
prefix := netip.MustParsePrefix(pair.source)
|
||||||
|
|
||||||
|
sourceExp := generateCIDRMatcherExpressions("source", pair.source)
|
||||||
|
destExp := generateCIDRMatcherExpressions("destination", pair.destination)
|
||||||
|
|
||||||
|
var expression []expr.Any
|
||||||
|
if isNat {
|
||||||
|
expression = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
||||||
|
} else {
|
||||||
|
expression = append(sourceExp, append(destExp, exprCounterAccept...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleKey := genKey(format, pair.ID)
|
||||||
|
|
||||||
|
_, exists := n.rules[ruleKey]
|
||||||
|
if exists {
|
||||||
|
err := n.removeRoutingRule(format, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix.Addr().Unmap().Is4() {
|
||||||
|
n.rules[ruleKey] = n.conn.InsertRule(&nftables.Rule{
|
||||||
|
Table: n.tableIPv4,
|
||||||
|
Chain: n.chains[ipv4][chain],
|
||||||
|
Exprs: expression,
|
||||||
|
UserData: []byte(ruleKey),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
n.rules[ruleKey] = n.conn.InsertRule(&nftables.Rule{
|
||||||
|
Table: n.tableIPv6,
|
||||||
|
Chain: n.chains[ipv6][chain],
|
||||||
|
Exprs: expression,
|
||||||
|
UserData: []byte(ruleKey),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRoutingRules removes a nftable rule pair from forwarding and nat chains
|
||||||
|
func (n *nftablesManager) RemoveRoutingRules(pair routerPair) error {
|
||||||
|
n.mux.Lock()
|
||||||
|
defer n.mux.Unlock()
|
||||||
|
|
||||||
|
err := n.refreshRulesMap()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.removeRoutingRule(forwardingFormat, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.removeRoutingRule(inForwardingFormat, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.removeRoutingRule(natFormat, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.removeRoutingRule(inNatFormat, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.conn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: received error while applying rule removal for %s: %v", pair.destination, err)
|
||||||
|
}
|
||||||
|
log.Debugf("nftables: removed rules for %s", pair.destination)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeRoutingRule add a nftable rule to the removal queue and delete from rules map
|
||||||
|
func (n *nftablesManager) removeRoutingRule(format string, pair routerPair) error {
|
||||||
|
ruleKey := genKey(format, pair.ID)
|
||||||
|
|
||||||
|
rule, found := n.rules[ruleKey]
|
||||||
|
if found {
|
||||||
|
ruleType := "forwarding"
|
||||||
|
if rule.Chain.Type == nftables.ChainTypeNAT {
|
||||||
|
ruleType = "nat"
|
||||||
|
}
|
||||||
|
|
||||||
|
err := n.conn.DelRule(rule)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to remove %s rule for %s: %v", ruleType, pair.destination, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("nftables: removing %s rule for %s", ruleType, pair.destination)
|
||||||
|
|
||||||
|
delete(n.rules, ruleKey)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPayloadDirectives get expression directives based on ip version and direction
|
||||||
|
func getPayloadDirectives(direction string, isIPv4 bool, isIPv6 bool) (uint32, uint32, []byte) {
|
||||||
|
switch {
|
||||||
|
case direction == exprDirectionSource && isIPv4:
|
||||||
|
return ipv4SrcOffset, ipv4Len, zeroXor
|
||||||
|
case direction == exprDirectionDestination && isIPv4:
|
||||||
|
return ipv4DestOffset, ipv4Len, zeroXor
|
||||||
|
case direction == exprDirectionSource && isIPv6:
|
||||||
|
return ipv6SrcOffset, ipv6Len, zeroXor6
|
||||||
|
case direction == exprDirectionDestination && isIPv6:
|
||||||
|
return ipv6DestOffset, ipv6Len, zeroXor6
|
||||||
|
default:
|
||||||
|
panic("no matched payload directive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCIDRMatcherExpressions generates nftables expressions that matches a CIDR
|
||||||
|
func generateCIDRMatcherExpressions(direction string, cidr string) []expr.Any {
|
||||||
|
ip, network, _ := net.ParseCIDR(cidr)
|
||||||
|
ipToAdd, _ := netip.AddrFromSlice(ip)
|
||||||
|
add := ipToAdd.Unmap()
|
||||||
|
|
||||||
|
offSet, packetLen, zeroXor := getPayloadDirectives(direction, add.Is4(), add.Is6())
|
||||||
|
|
||||||
|
return []expr.Any{
|
||||||
|
// fetch src add
|
||||||
|
&expr.Payload{
|
||||||
|
DestRegister: 1,
|
||||||
|
Base: expr.PayloadBaseNetworkHeader,
|
||||||
|
Offset: offSet,
|
||||||
|
Len: packetLen,
|
||||||
|
},
|
||||||
|
// net mask
|
||||||
|
&expr.Bitwise{
|
||||||
|
DestRegister: 1,
|
||||||
|
SourceRegister: 1,
|
||||||
|
Len: packetLen,
|
||||||
|
Mask: network.Mask,
|
||||||
|
Xor: zeroXor,
|
||||||
|
},
|
||||||
|
// net address
|
||||||
|
&expr.Cmp{
|
||||||
|
Register: 1,
|
||||||
|
Data: add.AsSlice(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
333
client/internal/routemanager/nftables_linux_test.go
Normal file
333
client/internal/routemanager/nftables_linux_test.go
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/google/nftables"
|
||||||
|
"github.com/google/nftables/expr"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNftablesManager_RestoreOrCreateContainers(t *testing.T) {
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
|
||||||
|
manager := &nftablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
conn: &nftables.Conn{},
|
||||||
|
chains: make(map[string]map[string]*nftables.Chain),
|
||||||
|
rules: make(map[string]*nftables.Rule),
|
||||||
|
}
|
||||||
|
|
||||||
|
nftablesTestingClient := &nftables.Conn{}
|
||||||
|
|
||||||
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
err := manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
require.Len(t, manager.chains, 2, "should have created chains for ipv4 and ipv6")
|
||||||
|
require.Len(t, manager.chains[ipv4], 2, "should have created chains for ipv4")
|
||||||
|
require.Len(t, manager.chains[ipv4], 2, "should have created chains for ipv6")
|
||||||
|
require.Len(t, manager.rules, 2, "should have created rules for ipv4 and ipv6")
|
||||||
|
|
||||||
|
pair := routerPair{
|
||||||
|
ID: "abc",
|
||||||
|
source: "100.100.100.1/32",
|
||||||
|
destination: "100.100.100.0/24",
|
||||||
|
masquerade: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceExp := generateCIDRMatcherExpressions("source", pair.source)
|
||||||
|
destExp := generateCIDRMatcherExpressions("destination", pair.destination)
|
||||||
|
|
||||||
|
forward4Exp := append(sourceExp, append(destExp, exprCounterAccept...)...)
|
||||||
|
forward4RuleKey := genKey(forwardingFormat, pair.ID)
|
||||||
|
inserted4Forwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: manager.tableIPv4,
|
||||||
|
Chain: manager.chains[ipv4][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: forward4Exp,
|
||||||
|
UserData: []byte(forward4RuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
nat4Exp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
||||||
|
nat4RuleKey := genKey(natFormat, pair.ID)
|
||||||
|
|
||||||
|
inserted4Nat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: manager.tableIPv4,
|
||||||
|
Chain: manager.chains[ipv4][nftablesRoutingNatChain],
|
||||||
|
Exprs: nat4Exp,
|
||||||
|
UserData: []byte(nat4RuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
err = nftablesTestingClient.Flush()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
pair = routerPair{
|
||||||
|
ID: "xyz",
|
||||||
|
source: "fc00::1/128",
|
||||||
|
destination: "fc11::/64",
|
||||||
|
masquerade: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceExp = generateCIDRMatcherExpressions("source", pair.source)
|
||||||
|
destExp = generateCIDRMatcherExpressions("destination", pair.destination)
|
||||||
|
|
||||||
|
forward6Exp := append(sourceExp, append(destExp, exprCounterAccept...)...)
|
||||||
|
forward6RuleKey := genKey(forwardingFormat, pair.ID)
|
||||||
|
inserted6Forwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: manager.tableIPv6,
|
||||||
|
Chain: manager.chains[ipv6][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: forward6Exp,
|
||||||
|
UserData: []byte(forward6RuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
nat6Exp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
||||||
|
nat6RuleKey := genKey(natFormat, pair.ID)
|
||||||
|
|
||||||
|
inserted6Nat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: manager.tableIPv6,
|
||||||
|
Chain: manager.chains[ipv6][nftablesRoutingNatChain],
|
||||||
|
Exprs: nat6Exp,
|
||||||
|
UserData: []byte(nat6RuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
err = nftablesTestingClient.Flush()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
manager.tableIPv4 = nil
|
||||||
|
manager.tableIPv6 = nil
|
||||||
|
|
||||||
|
err = manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
require.Len(t, manager.chains, 2, "should have created chains for ipv4 and ipv6")
|
||||||
|
require.Len(t, manager.chains[ipv4], 2, "should have created chains for ipv4")
|
||||||
|
require.Len(t, manager.chains[ipv4], 2, "should have created chains for ipv6")
|
||||||
|
require.Len(t, manager.rules, 6, "should have restored all rules for ipv4 and ipv6")
|
||||||
|
|
||||||
|
foundRule, found := manager.rules[forward4RuleKey]
|
||||||
|
require.True(t, found, "forwarding rule should exist in the map")
|
||||||
|
assert.Equal(t, inserted4Forwarding.Exprs, foundRule.Exprs, "stored forwarding rule expressions should match")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[nat4RuleKey]
|
||||||
|
require.True(t, found, "nat rule should exist in the map")
|
||||||
|
// match len of output as nftables client doesn't return expressions with masquerade expression
|
||||||
|
assert.ElementsMatch(t, inserted4Nat.Exprs[:len(foundRule.Exprs)], foundRule.Exprs, "stored nat rule expressions should match")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[forward6RuleKey]
|
||||||
|
require.True(t, found, "forwarding rule should exist in the map")
|
||||||
|
assert.Equal(t, inserted6Forwarding.Exprs, foundRule.Exprs, "stored forward rule should match")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[nat6RuleKey]
|
||||||
|
require.True(t, found, "nat rule should exist in the map")
|
||||||
|
// match len of output as nftables client doesn't return expressions with masquerade expression
|
||||||
|
assert.ElementsMatch(t, inserted6Nat.Exprs[:len(foundRule.Exprs)], foundRule.Exprs, "stored nat rule should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNftablesManager_InsertRoutingRules(t *testing.T) {
|
||||||
|
|
||||||
|
for _, testCase := range insertRuleTestCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
|
||||||
|
manager := &nftablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
conn: &nftables.Conn{},
|
||||||
|
chains: make(map[string]map[string]*nftables.Chain),
|
||||||
|
rules: make(map[string]*nftables.Rule),
|
||||||
|
}
|
||||||
|
|
||||||
|
nftablesTestingClient := &nftables.Conn{}
|
||||||
|
|
||||||
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
err := manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
err = manager.InsertRoutingRules(testCase.inputPair)
|
||||||
|
require.NoError(t, err, "forwarding pair should be inserted")
|
||||||
|
|
||||||
|
sourceExp := generateCIDRMatcherExpressions("source", testCase.inputPair.source)
|
||||||
|
destExp := generateCIDRMatcherExpressions("destination", testCase.inputPair.destination)
|
||||||
|
testingExpression := append(sourceExp, destExp...)
|
||||||
|
fwdRuleKey := genKey(forwardingFormat, testCase.inputPair.ID)
|
||||||
|
|
||||||
|
found := 0
|
||||||
|
for _, registeredChains := range manager.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 && string(rule.UserData) == fwdRuleKey {
|
||||||
|
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "forwarding rule elements should match")
|
||||||
|
found = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
||||||
|
|
||||||
|
if testCase.inputPair.masquerade {
|
||||||
|
natRuleKey := genKey(natFormat, testCase.inputPair.ID)
|
||||||
|
found := 0
|
||||||
|
for _, registeredChains := range manager.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 && string(rule.UserData) == natRuleKey {
|
||||||
|
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "nat rule elements should match")
|
||||||
|
found = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceExp = generateCIDRMatcherExpressions("source", getInPair(testCase.inputPair).source)
|
||||||
|
destExp = generateCIDRMatcherExpressions("destination", getInPair(testCase.inputPair).destination)
|
||||||
|
testingExpression = append(sourceExp, destExp...)
|
||||||
|
inFwdRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID)
|
||||||
|
|
||||||
|
found = 0
|
||||||
|
for _, registeredChains := range manager.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 && string(rule.UserData) == inFwdRuleKey {
|
||||||
|
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "income forwarding rule elements should match")
|
||||||
|
found = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
||||||
|
|
||||||
|
if testCase.inputPair.masquerade {
|
||||||
|
inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID)
|
||||||
|
found := 0
|
||||||
|
for _, registeredChains := range manager.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 && string(rule.UserData) == inNatRuleKey {
|
||||||
|
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "income nat rule elements should match")
|
||||||
|
found = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
|
||||||
|
|
||||||
|
for _, testCase := range removeRuleTestCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
|
||||||
|
manager := &nftablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
conn: &nftables.Conn{},
|
||||||
|
chains: make(map[string]map[string]*nftables.Chain),
|
||||||
|
rules: make(map[string]*nftables.Rule),
|
||||||
|
}
|
||||||
|
|
||||||
|
nftablesTestingClient := &nftables.Conn{}
|
||||||
|
|
||||||
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
err := manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
table := manager.tableIPv4
|
||||||
|
if testCase.ipVersion == ipv6 {
|
||||||
|
table = manager.tableIPv6
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceExp := generateCIDRMatcherExpressions("source", testCase.inputPair.source)
|
||||||
|
destExp := generateCIDRMatcherExpressions("destination", testCase.inputPair.destination)
|
||||||
|
|
||||||
|
forwardExp := append(sourceExp, append(destExp, exprCounterAccept...)...)
|
||||||
|
forwardRuleKey := genKey(forwardingFormat, testCase.inputPair.ID)
|
||||||
|
insertedForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: table,
|
||||||
|
Chain: manager.chains[testCase.ipVersion][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: forwardExp,
|
||||||
|
UserData: []byte(forwardRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
natExp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
||||||
|
natRuleKey := genKey(natFormat, testCase.inputPair.ID)
|
||||||
|
|
||||||
|
insertedNat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: table,
|
||||||
|
Chain: manager.chains[testCase.ipVersion][nftablesRoutingNatChain],
|
||||||
|
Exprs: natExp,
|
||||||
|
UserData: []byte(natRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
sourceExp = generateCIDRMatcherExpressions("source", getInPair(testCase.inputPair).source)
|
||||||
|
destExp = generateCIDRMatcherExpressions("destination", getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
forwardExp = append(sourceExp, append(destExp, exprCounterAccept...)...)
|
||||||
|
inForwardRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID)
|
||||||
|
insertedInForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: table,
|
||||||
|
Chain: manager.chains[testCase.ipVersion][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: forwardExp,
|
||||||
|
UserData: []byte(inForwardRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
natExp = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
||||||
|
inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID)
|
||||||
|
|
||||||
|
insertedInNat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: table,
|
||||||
|
Chain: manager.chains[testCase.ipVersion][nftablesRoutingNatChain],
|
||||||
|
Exprs: natExp,
|
||||||
|
UserData: []byte(inNatRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
err = nftablesTestingClient.Flush()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
manager.tableIPv4 = nil
|
||||||
|
manager.tableIPv6 = nil
|
||||||
|
|
||||||
|
err = manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
err = manager.RemoveRoutingRules(testCase.inputPair)
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
for _, registeredChains := range manager.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 {
|
||||||
|
require.NotEqual(t, insertedForwarding.UserData, rule.UserData, "forwarding rule should not exist")
|
||||||
|
require.NotEqual(t, insertedNat.UserData, rule.UserData, "nat rule should not exist")
|
||||||
|
require.NotEqual(t, insertedInForwarding.UserData, rule.UserData, "income forwarding rule should not exist")
|
||||||
|
require.NotEqual(t, insertedInNat.UserData, rule.UserData, "income nat rule should not exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
67
client/internal/routemanager/server.go
Normal file
67
client/internal/routemanager/server.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serverRouter struct {
|
||||||
|
routes map[string]*route.Route
|
||||||
|
// best effort to keep net forward configuration as it was
|
||||||
|
netForwardHistoryEnabled bool
|
||||||
|
mux sync.Mutex
|
||||||
|
firewall firewallManager
|
||||||
|
}
|
||||||
|
|
||||||
|
type routerPair struct {
|
||||||
|
ID string
|
||||||
|
source string
|
||||||
|
destination string
|
||||||
|
masquerade bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func routeToRouterPair(source string, route *route.Route) routerPair {
|
||||||
|
parsed := netip.MustParsePrefix(source).Masked()
|
||||||
|
return routerPair{
|
||||||
|
ID: route.ID,
|
||||||
|
source: parsed.String(),
|
||||||
|
destination: route.Network.Masked().String(),
|
||||||
|
masquerade: route.Masquerade,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DefaultManager) removeFromServerNetwork(route *route.Route) error {
|
||||||
|
select {
|
||||||
|
case <-m.ctx.Done():
|
||||||
|
log.Infof("not removing from server network because context is done")
|
||||||
|
return m.ctx.Err()
|
||||||
|
default:
|
||||||
|
m.serverRouter.mux.Lock()
|
||||||
|
defer m.serverRouter.mux.Unlock()
|
||||||
|
err := m.serverRouter.firewall.RemoveRoutingRules(routeToRouterPair(m.wgInterface.Address.String(), route))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(m.serverRouter.routes, route.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DefaultManager) addToServerNetwork(route *route.Route) error {
|
||||||
|
select {
|
||||||
|
case <-m.ctx.Done():
|
||||||
|
log.Infof("not adding to server network because context is done")
|
||||||
|
return m.ctx.Err()
|
||||||
|
default:
|
||||||
|
m.serverRouter.mux.Lock()
|
||||||
|
defer m.serverRouter.mux.Unlock()
|
||||||
|
err := m.serverRouter.firewall.InsertRoutingRules(routeToRouterPair(m.wgInterface.Address.String(), route))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.serverRouter.routes[route.ID] = route
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
58
client/internal/routemanager/systemops.go
Normal file
58
client/internal/routemanager/systemops.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/libp2p/go-netroute"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errRouteNotFound = fmt.Errorf("route not found")
|
||||||
|
|
||||||
|
func addToRouteTableIfNoExists(prefix netip.Prefix, addr string) error {
|
||||||
|
gateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0"))
|
||||||
|
if err != nil && err != errRouteNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
prefixGateway, err := getExistingRIBRouteGateway(prefix)
|
||||||
|
if err != nil && err != errRouteNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefixGateway != nil && !prefixGateway.Equal(gateway) {
|
||||||
|
log.Warnf("skipping adding a new route for network %s because it already exists and is pointing to the non default gateway: %s", prefix, prefixGateway)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return addToRouteTable(prefix, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFromRouteTableIfNonSystem(prefix netip.Prefix, addr string) error {
|
||||||
|
addrIP := net.ParseIP(addr)
|
||||||
|
prefixGateway, err := getExistingRIBRouteGateway(prefix)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if prefixGateway != nil && !prefixGateway.Equal(addrIP) {
|
||||||
|
log.Warnf("route for network %s is pointing to a different gateway: %s, should be pointing to: %s, not removing", prefix, prefixGateway, addrIP)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return removeFromRouteTable(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExistingRIBRouteGateway(prefix netip.Prefix) (net.IP, error) {
|
||||||
|
r, err := netroute.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, gateway, preferredSrc, err := r.Route(prefix.Addr().AsSlice())
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("getting routes returned an error: %v", err)
|
||||||
|
return nil, errRouteNotFound
|
||||||
|
}
|
||||||
|
if gateway == nil {
|
||||||
|
return preferredSrc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gateway, nil
|
||||||
|
}
|
||||||
73
client/internal/routemanager/systemops_linux.go
Normal file
73
client/internal/routemanager/systemops_linux.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ipv4ForwardingPath = "/proc/sys/net/ipv4/ip_forward"
|
||||||
|
|
||||||
|
func addToRouteTable(prefix netip.Prefix, addr string) error {
|
||||||
|
_, ipNet, err := net.ParseCIDR(prefix.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrMask := "/32"
|
||||||
|
if prefix.Addr().Unmap().Is6() {
|
||||||
|
addrMask = "/128"
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, _, err := net.ParseCIDR(addr + addrMask)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
route := &netlink.Route{
|
||||||
|
Scope: netlink.SCOPE_UNIVERSE,
|
||||||
|
Dst: ipNet,
|
||||||
|
Gw: ip,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = netlink.RouteAdd(route)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFromRouteTable(prefix netip.Prefix) error {
|
||||||
|
_, ipNet, err := net.ParseCIDR(prefix.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
route := &netlink.Route{
|
||||||
|
Scope: netlink.SCOPE_UNIVERSE,
|
||||||
|
Dst: ipNet,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = netlink.RouteDel(route)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableIPForwarding() error {
|
||||||
|
err := ioutil.WriteFile(ipv4ForwardingPath, []byte("1"), 0644)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNetForwardHistoryEnabled() bool {
|
||||||
|
out, err := ioutil.ReadFile(ipv4ForwardingPath)
|
||||||
|
if err != nil {
|
||||||
|
// todo
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(out) == "1"
|
||||||
|
}
|
||||||
41
client/internal/routemanager/systemops_nonlinux.go
Normal file
41
client/internal/routemanager/systemops_nonlinux.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//go:build !linux
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net/netip"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addToRouteTable(prefix netip.Prefix, addr string) error {
|
||||||
|
cmd := exec.Command("route", "add", prefix.String(), addr)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf(string(out))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFromRouteTable(prefix netip.Prefix) error {
|
||||||
|
cmd := exec.Command("route", "delete", prefix.String())
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf(string(out))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableIPForwarding() error {
|
||||||
|
log.Infof("enable IP forwarding is not implemented on %s", runtime.GOOS)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNetForwardHistoryEnabled() bool {
|
||||||
|
log.Infof("check netforwad history is not implemented on %s", runtime.GOOS)
|
||||||
|
return false
|
||||||
|
}
|
||||||
111
client/internal/routemanager/systemops_test.go
Normal file
111
client/internal/routemanager/systemops_test.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddRemoveRoutes(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
prefix netip.Prefix
|
||||||
|
shouldRouteToWireguard bool
|
||||||
|
shouldBeRemoved bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Should Add And Remove Route",
|
||||||
|
prefix: netip.MustParsePrefix("100.66.120.0/24"),
|
||||||
|
shouldRouteToWireguard: true,
|
||||||
|
shouldBeRemoved: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Add Or Remove Route",
|
||||||
|
prefix: netip.MustParsePrefix("127.0.0.1/32"),
|
||||||
|
shouldRouteToWireguard: false,
|
||||||
|
shouldBeRemoved: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun53%d", n), "100.65.75.2/24", iface.DefaultMTU)
|
||||||
|
require.NoError(t, err, "should create testing WGIface interface")
|
||||||
|
defer wgInterface.Close()
|
||||||
|
|
||||||
|
err = wgInterface.Create()
|
||||||
|
require.NoError(t, err, "should create testing wireguard interface")
|
||||||
|
|
||||||
|
err = addToRouteTableIfNoExists(testCase.prefix, wgInterface.GetAddress().IP.String())
|
||||||
|
require.NoError(t, err, "should not return err")
|
||||||
|
|
||||||
|
prefixGateway, err := getExistingRIBRouteGateway(testCase.prefix)
|
||||||
|
require.NoError(t, err, "should not return err")
|
||||||
|
if testCase.shouldRouteToWireguard {
|
||||||
|
require.Equal(t, wgInterface.GetAddress().IP.String(), prefixGateway.String(), "route should point to wireguard interface IP")
|
||||||
|
} else {
|
||||||
|
require.NotEqual(t, wgInterface.GetAddress().IP.String(), prefixGateway.String(), "route should point to a different interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = removeFromRouteTableIfNonSystem(testCase.prefix, wgInterface.GetAddress().IP.String())
|
||||||
|
require.NoError(t, err, "should not return err")
|
||||||
|
|
||||||
|
prefixGateway, err = getExistingRIBRouteGateway(testCase.prefix)
|
||||||
|
require.NoError(t, err, "should not return err")
|
||||||
|
|
||||||
|
internetGateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if testCase.shouldBeRemoved {
|
||||||
|
require.Equal(t, internetGateway, prefixGateway, "route should be pointing to default internet gateway")
|
||||||
|
} else {
|
||||||
|
require.NotEqual(t, internetGateway, prefixGateway, "route should be pointing to a different gateway than the internet gateway")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetExistingRIBRouteGateway(t *testing.T) {
|
||||||
|
gateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("shouldn't return error when fetching the gateway: ", err)
|
||||||
|
}
|
||||||
|
if gateway == nil {
|
||||||
|
t.Fatal("should return a gateway")
|
||||||
|
}
|
||||||
|
addresses, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("shouldn't return error when fetching interface addresses: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var testingIP string
|
||||||
|
var testingPrefix netip.Prefix
|
||||||
|
for _, address := range addresses {
|
||||||
|
if address.Network() != "ip+net" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prefix := netip.MustParsePrefix(address.String())
|
||||||
|
if !prefix.Addr().IsLoopback() && prefix.Addr().Is4() {
|
||||||
|
testingIP = prefix.Addr().String()
|
||||||
|
testingPrefix = prefix.Masked()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localIP, err := getExistingRIBRouteGateway(testingPrefix)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("shouldn't return error: ", err)
|
||||||
|
}
|
||||||
|
if localIP == nil {
|
||||||
|
t.Fatal("should return a gateway for local network")
|
||||||
|
}
|
||||||
|
if localIP.String() == gateway.String() {
|
||||||
|
t.Fatal("local ip should not match with gateway IP")
|
||||||
|
}
|
||||||
|
if localIP.String() != testingIP {
|
||||||
|
t.Fatalf("local ip should match with testing IP: want %s got %s", testingIP, localIP.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/cmd"
|
"github.com/netbirdio/netbird/client/cmd"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.26.0
|
// protoc-gen-go v1.26.0
|
||||||
// protoc v3.19.4
|
// protoc v3.21.9
|
||||||
// source: daemon.proto
|
// source: daemon.proto
|
||||||
|
|
||||||
package proto
|
package proto
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
_ "google.golang.org/protobuf/types/descriptorpb"
|
_ "google.golang.org/protobuf/types/descriptorpb"
|
||||||
|
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
sync "sync"
|
sync "sync"
|
||||||
)
|
)
|
||||||
@@ -332,6 +333,8 @@ type StatusRequest struct {
|
|||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
GetFullPeerStatus bool `protobuf:"varint,1,opt,name=getFullPeerStatus,proto3" json:"getFullPeerStatus,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *StatusRequest) Reset() {
|
func (x *StatusRequest) Reset() {
|
||||||
@@ -366,6 +369,13 @@ func (*StatusRequest) Descriptor() ([]byte, []int) {
|
|||||||
return file_daemon_proto_rawDescGZIP(), []int{6}
|
return file_daemon_proto_rawDescGZIP(), []int{6}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *StatusRequest) GetGetFullPeerStatus() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.GetFullPeerStatus
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type StatusResponse struct {
|
type StatusResponse struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -373,6 +383,9 @@ type StatusResponse struct {
|
|||||||
|
|
||||||
// status of the server.
|
// status of the server.
|
||||||
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
|
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
|
||||||
|
FullStatus *FullStatus `protobuf:"bytes,2,opt,name=fullStatus,proto3" json:"fullStatus,omitempty"`
|
||||||
|
// NetBird daemon version
|
||||||
|
DaemonVersion string `protobuf:"bytes,3,opt,name=daemonVersion,proto3" json:"daemonVersion,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *StatusResponse) Reset() {
|
func (x *StatusResponse) Reset() {
|
||||||
@@ -414,6 +427,20 @@ func (x *StatusResponse) GetStatus() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *StatusResponse) GetFullStatus() *FullStatus {
|
||||||
|
if x != nil {
|
||||||
|
return x.FullStatus
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *StatusResponse) GetDaemonVersion() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.DaemonVersion
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type DownRequest struct {
|
type DownRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -612,83 +639,514 @@ func (x *GetConfigResponse) GetAdminURL() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PeerState contains the latest state of a peer
|
||||||
|
type PeerState struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"`
|
||||||
|
PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"`
|
||||||
|
ConnStatus string `protobuf:"bytes,3,opt,name=connStatus,proto3" json:"connStatus,omitempty"`
|
||||||
|
ConnStatusUpdate *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=connStatusUpdate,proto3" json:"connStatusUpdate,omitempty"`
|
||||||
|
Relayed bool `protobuf:"varint,5,opt,name=relayed,proto3" json:"relayed,omitempty"`
|
||||||
|
Direct bool `protobuf:"varint,6,opt,name=direct,proto3" json:"direct,omitempty"`
|
||||||
|
LocalIceCandidateType string `protobuf:"bytes,7,opt,name=localIceCandidateType,proto3" json:"localIceCandidateType,omitempty"`
|
||||||
|
RemoteIceCandidateType string `protobuf:"bytes,8,opt,name=remoteIceCandidateType,proto3" json:"remoteIceCandidateType,omitempty"`
|
||||||
|
Fqdn string `protobuf:"bytes,9,opt,name=fqdn,proto3" json:"fqdn,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PeerState) Reset() {
|
||||||
|
*x = PeerState{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_daemon_proto_msgTypes[12]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PeerState) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*PeerState) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *PeerState) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_daemon_proto_msgTypes[12]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use PeerState.ProtoReflect.Descriptor instead.
|
||||||
|
func (*PeerState) Descriptor() ([]byte, []int) {
|
||||||
|
return file_daemon_proto_rawDescGZIP(), []int{12}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PeerState) GetIP() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.IP
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PeerState) GetPubKey() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.PubKey
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PeerState) GetConnStatus() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ConnStatus
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PeerState) GetConnStatusUpdate() *timestamppb.Timestamp {
|
||||||
|
if x != nil {
|
||||||
|
return x.ConnStatusUpdate
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PeerState) GetRelayed() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Relayed
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PeerState) GetDirect() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Direct
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PeerState) GetLocalIceCandidateType() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.LocalIceCandidateType
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PeerState) GetRemoteIceCandidateType() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.RemoteIceCandidateType
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PeerState) GetFqdn() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Fqdn
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalPeerState contains the latest state of the local peer
|
||||||
|
type LocalPeerState struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"`
|
||||||
|
PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"`
|
||||||
|
KernelInterface bool `protobuf:"varint,3,opt,name=kernelInterface,proto3" json:"kernelInterface,omitempty"`
|
||||||
|
Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LocalPeerState) Reset() {
|
||||||
|
*x = LocalPeerState{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_daemon_proto_msgTypes[13]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LocalPeerState) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*LocalPeerState) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *LocalPeerState) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_daemon_proto_msgTypes[13]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use LocalPeerState.ProtoReflect.Descriptor instead.
|
||||||
|
func (*LocalPeerState) Descriptor() ([]byte, []int) {
|
||||||
|
return file_daemon_proto_rawDescGZIP(), []int{13}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LocalPeerState) GetIP() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.IP
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LocalPeerState) GetPubKey() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.PubKey
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LocalPeerState) GetKernelInterface() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.KernelInterface
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LocalPeerState) GetFqdn() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Fqdn
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignalState contains the latest state of a signal connection
|
||||||
|
type SignalState struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
URL string `protobuf:"bytes,1,opt,name=URL,proto3" json:"URL,omitempty"`
|
||||||
|
Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignalState) Reset() {
|
||||||
|
*x = SignalState{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_daemon_proto_msgTypes[14]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignalState) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SignalState) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SignalState) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_daemon_proto_msgTypes[14]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SignalState.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SignalState) Descriptor() ([]byte, []int) {
|
||||||
|
return file_daemon_proto_rawDescGZIP(), []int{14}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignalState) GetURL() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.URL
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignalState) GetConnected() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Connected
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagementState contains the latest state of a management connection
|
||||||
|
type ManagementState struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
URL string `protobuf:"bytes,1,opt,name=URL,proto3" json:"URL,omitempty"`
|
||||||
|
Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ManagementState) Reset() {
|
||||||
|
*x = ManagementState{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_daemon_proto_msgTypes[15]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ManagementState) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ManagementState) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ManagementState) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_daemon_proto_msgTypes[15]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ManagementState.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ManagementState) Descriptor() ([]byte, []int) {
|
||||||
|
return file_daemon_proto_rawDescGZIP(), []int{15}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ManagementState) GetURL() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.URL
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ManagementState) GetConnected() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Connected
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// FullStatus contains the full state held by the Status instance
|
||||||
|
type FullStatus struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
ManagementState *ManagementState `protobuf:"bytes,1,opt,name=managementState,proto3" json:"managementState,omitempty"`
|
||||||
|
SignalState *SignalState `protobuf:"bytes,2,opt,name=signalState,proto3" json:"signalState,omitempty"`
|
||||||
|
LocalPeerState *LocalPeerState `protobuf:"bytes,3,opt,name=localPeerState,proto3" json:"localPeerState,omitempty"`
|
||||||
|
Peers []*PeerState `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FullStatus) Reset() {
|
||||||
|
*x = FullStatus{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_daemon_proto_msgTypes[16]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FullStatus) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*FullStatus) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *FullStatus) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_daemon_proto_msgTypes[16]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use FullStatus.ProtoReflect.Descriptor instead.
|
||||||
|
func (*FullStatus) Descriptor() ([]byte, []int) {
|
||||||
|
return file_daemon_proto_rawDescGZIP(), []int{16}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FullStatus) GetManagementState() *ManagementState {
|
||||||
|
if x != nil {
|
||||||
|
return x.ManagementState
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FullStatus) GetSignalState() *SignalState {
|
||||||
|
if x != nil {
|
||||||
|
return x.SignalState
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FullStatus) GetLocalPeerState() *LocalPeerState {
|
||||||
|
if x != nil {
|
||||||
|
return x.LocalPeerState
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FullStatus) GetPeers() []*PeerState {
|
||||||
|
if x != nil {
|
||||||
|
return x.Peers
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var File_daemon_proto protoreflect.FileDescriptor
|
var File_daemon_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
var file_daemon_proto_rawDesc = []byte{
|
var file_daemon_proto_rawDesc = []byte{
|
||||||
0x0a, 0x0c, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06,
|
0x0a, 0x0c, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06,
|
||||||
0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
|
0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
|
||||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
|
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
|
||||||
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x90, 0x01, 0x0a, 0x0c, 0x4c, 0x6f, 0x67,
|
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||||
0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x74,
|
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
|
||||||
0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x74,
|
0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x90, 0x01, 0x0a, 0x0c, 0x4c, 0x6f,
|
||||||
0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72,
|
0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65,
|
||||||
0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65,
|
0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65,
|
||||||
0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e,
|
0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61,
|
||||||
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
|
0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72,
|
||||||
0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12,
|
0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61,
|
||||||
0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28,
|
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||||
0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x22, 0xb5, 0x01, 0x0a, 0x0d,
|
0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c,
|
||||||
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a,
|
0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01,
|
||||||
0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01,
|
0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x22, 0xb5, 0x01, 0x0a,
|
||||||
0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f,
|
0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24,
|
||||||
0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18,
|
0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18,
|
||||||
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12,
|
0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c,
|
||||||
0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55,
|
0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
|
||||||
0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69,
|
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
|
||||||
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72,
|
0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||||
|
0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66,
|
||||||
|
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65,
|
||||||
|
0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d,
|
||||||
|
0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72,
|
||||||
0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70,
|
0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70,
|
||||||
0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69,
|
0x6c, 0x65, 0x74, 0x65, 0x22, 0x31, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c,
|
||||||
0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c,
|
0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75,
|
||||||
0x65, 0x74, 0x65, 0x22, 0x31, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f,
|
0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75,
|
||||||
0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73,
|
0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53,
|
||||||
0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73,
|
0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
||||||
0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53,
|
0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a,
|
||||||
0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b,
|
0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74,
|
||||||
0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55,
|
0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67,
|
||||||
0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x53, 0x74, 0x61,
|
0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
|
||||||
0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x28, 0x0a, 0x0e, 0x53, 0x74,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50,
|
||||||
|
0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74,
|
||||||
0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06,
|
0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06,
|
||||||
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74,
|
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74,
|
||||||
0x61, 0x74, 0x75, 0x73, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75,
|
0x61, 0x74, 0x75, 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74,
|
||||||
0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
|
||||||
0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75,
|
||||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb3, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43,
|
0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d,
|
||||||
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a,
|
0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01,
|
0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
|
0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a,
|
||||||
0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c,
|
0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a,
|
||||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46,
|
0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03,
|
0x74, 0x22, 0xb3, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a,
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67,
|
||||||
0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20,
|
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65,
|
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a,
|
||||||
0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20,
|
0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x32, 0xf7, 0x02,
|
0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a,
|
||||||
0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
|
0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
|
||||||
0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
|
0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68,
|
||||||
0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15,
|
0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70,
|
||||||
0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73,
|
0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61,
|
||||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53,
|
0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61,
|
||||||
0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
|
0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x22, 0xcf, 0x02, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72,
|
||||||
0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71,
|
0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61,
|
0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18,
|
||||||
0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a,
|
||||||
0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65,
|
0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||||
0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e,
|
0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a,
|
||||||
0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74,
|
||||||
0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e,
|
0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||||
0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71,
|
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
|
||||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74,
|
0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55,
|
||||||
0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33,
|
0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64,
|
||||||
0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
|
0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12,
|
||||||
0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61,
|
0x16, 0x0a, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52,
|
||||||
0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c,
|
||||||
0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65,
|
||||||
0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e,
|
0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65,
|
||||||
0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65,
|
0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a,
|
||||||
0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73,
|
0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64,
|
||||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72,
|
||||||
0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74,
|
||||||
|
0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20,
|
||||||
|
0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x76, 0x0a, 0x0e, 0x4c, 0x6f, 0x63,
|
||||||
|
0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49,
|
||||||
|
0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70,
|
||||||
|
0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62,
|
||||||
|
0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74,
|
||||||
|
0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65,
|
||||||
|
0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a,
|
||||||
|
0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64,
|
||||||
|
0x6e, 0x22, 0x3d, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65,
|
||||||
|
0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55,
|
||||||
|
0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18,
|
||||||
|
0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64,
|
||||||
|
0x22, 0x41, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
|
||||||
|
0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
|
0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74,
|
||||||
|
0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63,
|
||||||
|
0x74, 0x65, 0x64, 0x22, 0xef, 0x01, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74,
|
||||||
|
0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
|
||||||
|
0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61,
|
||||||
|
0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53,
|
||||||
|
0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
|
||||||
|
0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53,
|
||||||
|
0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65,
|
||||||
|
0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52,
|
||||||
|
0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e,
|
||||||
|
0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03,
|
||||||
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f,
|
||||||
|
0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f,
|
||||||
|
0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05,
|
||||||
|
0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61,
|
||||||
|
0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05,
|
||||||
|
0x70, 0x65, 0x65, 0x72, 0x73, 0x32, 0xf7, 0x02, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
|
||||||
|
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e,
|
||||||
|
0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
|
||||||
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
|
||||||
|
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
|
||||||
|
0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12,
|
||||||
|
0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f,
|
||||||
|
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64,
|
||||||
|
0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67,
|
||||||
|
0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02,
|
||||||
|
0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65,
|
||||||
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55,
|
||||||
|
0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53,
|
||||||
|
0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53,
|
||||||
|
0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64,
|
||||||
|
0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70,
|
||||||
|
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13,
|
||||||
|
0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75,
|
||||||
|
0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77,
|
||||||
|
0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47,
|
||||||
|
0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
|
||||||
|
0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
|
0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43,
|
||||||
|
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42,
|
||||||
|
0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
|
0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -703,7 +1161,7 @@ func file_daemon_proto_rawDescGZIP() []byte {
|
|||||||
return file_daemon_proto_rawDescData
|
return file_daemon_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
|
||||||
var file_daemon_proto_goTypes = []interface{}{
|
var file_daemon_proto_goTypes = []interface{}{
|
||||||
(*LoginRequest)(nil), // 0: daemon.LoginRequest
|
(*LoginRequest)(nil), // 0: daemon.LoginRequest
|
||||||
(*LoginResponse)(nil), // 1: daemon.LoginResponse
|
(*LoginResponse)(nil), // 1: daemon.LoginResponse
|
||||||
@@ -717,25 +1175,37 @@ var file_daemon_proto_goTypes = []interface{}{
|
|||||||
(*DownResponse)(nil), // 9: daemon.DownResponse
|
(*DownResponse)(nil), // 9: daemon.DownResponse
|
||||||
(*GetConfigRequest)(nil), // 10: daemon.GetConfigRequest
|
(*GetConfigRequest)(nil), // 10: daemon.GetConfigRequest
|
||||||
(*GetConfigResponse)(nil), // 11: daemon.GetConfigResponse
|
(*GetConfigResponse)(nil), // 11: daemon.GetConfigResponse
|
||||||
|
(*PeerState)(nil), // 12: daemon.PeerState
|
||||||
|
(*LocalPeerState)(nil), // 13: daemon.LocalPeerState
|
||||||
|
(*SignalState)(nil), // 14: daemon.SignalState
|
||||||
|
(*ManagementState)(nil), // 15: daemon.ManagementState
|
||||||
|
(*FullStatus)(nil), // 16: daemon.FullStatus
|
||||||
|
(*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp
|
||||||
}
|
}
|
||||||
var file_daemon_proto_depIdxs = []int32{
|
var file_daemon_proto_depIdxs = []int32{
|
||||||
0, // 0: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
|
16, // 0: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
|
||||||
2, // 1: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
|
17, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
|
||||||
4, // 2: daemon.DaemonService.Up:input_type -> daemon.UpRequest
|
15, // 2: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
|
||||||
6, // 3: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
|
14, // 3: daemon.FullStatus.signalState:type_name -> daemon.SignalState
|
||||||
8, // 4: daemon.DaemonService.Down:input_type -> daemon.DownRequest
|
13, // 4: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState
|
||||||
10, // 5: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
|
12, // 5: daemon.FullStatus.peers:type_name -> daemon.PeerState
|
||||||
1, // 6: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
|
0, // 6: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
|
||||||
3, // 7: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
|
2, // 7: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
|
||||||
5, // 8: daemon.DaemonService.Up:output_type -> daemon.UpResponse
|
4, // 8: daemon.DaemonService.Up:input_type -> daemon.UpRequest
|
||||||
7, // 9: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
|
6, // 9: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
|
||||||
9, // 10: daemon.DaemonService.Down:output_type -> daemon.DownResponse
|
8, // 10: daemon.DaemonService.Down:input_type -> daemon.DownRequest
|
||||||
11, // 11: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
|
10, // 11: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
|
||||||
6, // [6:12] is the sub-list for method output_type
|
1, // 12: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
|
||||||
0, // [0:6] is the sub-list for method input_type
|
3, // 13: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
|
||||||
0, // [0:0] is the sub-list for extension type_name
|
5, // 14: daemon.DaemonService.Up:output_type -> daemon.UpResponse
|
||||||
0, // [0:0] is the sub-list for extension extendee
|
7, // 15: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
|
||||||
0, // [0:0] is the sub-list for field type_name
|
9, // 16: daemon.DaemonService.Down:output_type -> daemon.DownResponse
|
||||||
|
11, // 17: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
|
||||||
|
12, // [12:18] is the sub-list for method output_type
|
||||||
|
6, // [6:12] is the sub-list for method input_type
|
||||||
|
6, // [6:6] is the sub-list for extension type_name
|
||||||
|
6, // [6:6] is the sub-list for extension extendee
|
||||||
|
0, // [0:6] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_daemon_proto_init() }
|
func init() { file_daemon_proto_init() }
|
||||||
@@ -888,6 +1358,66 @@ func file_daemon_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
file_daemon_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*PeerState); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_daemon_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*LocalPeerState); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_daemon_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*SignalState); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_daemon_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*ManagementState); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_daemon_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*FullStatus); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
@@ -895,7 +1425,7 @@ func file_daemon_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_daemon_proto_rawDesc,
|
RawDescriptor: file_daemon_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 12,
|
NumMessages: 17,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
import "google/protobuf/descriptor.proto";
|
import "google/protobuf/descriptor.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
option go_package = "/proto";
|
option go_package = "/proto";
|
||||||
|
|
||||||
@@ -59,11 +60,16 @@ message UpRequest {}
|
|||||||
|
|
||||||
message UpResponse {}
|
message UpResponse {}
|
||||||
|
|
||||||
message StatusRequest{}
|
message StatusRequest{
|
||||||
|
bool getFullPeerStatus = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message StatusResponse{
|
message StatusResponse{
|
||||||
// status of the server.
|
// status of the server.
|
||||||
string status = 1;
|
string status = 1;
|
||||||
|
FullStatus fullStatus = 2;
|
||||||
|
// NetBird daemon version
|
||||||
|
string daemonVersion = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DownRequest {}
|
message DownRequest {}
|
||||||
@@ -88,3 +94,43 @@ message GetConfigResponse {
|
|||||||
// adminURL settings value.
|
// adminURL settings value.
|
||||||
string adminURL = 5;
|
string adminURL = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PeerState contains the latest state of a peer
|
||||||
|
message PeerState {
|
||||||
|
string IP = 1;
|
||||||
|
string pubKey = 2;
|
||||||
|
string connStatus = 3;
|
||||||
|
google.protobuf.Timestamp connStatusUpdate = 4;
|
||||||
|
bool relayed = 5;
|
||||||
|
bool direct = 6;
|
||||||
|
string localIceCandidateType = 7;
|
||||||
|
string remoteIceCandidateType =8;
|
||||||
|
string fqdn = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalPeerState contains the latest state of the local peer
|
||||||
|
message LocalPeerState {
|
||||||
|
string IP = 1;
|
||||||
|
string pubKey = 2;
|
||||||
|
bool kernelInterface =3;
|
||||||
|
string fqdn = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignalState contains the latest state of a signal connection
|
||||||
|
message SignalState {
|
||||||
|
string URL = 1;
|
||||||
|
bool connected = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagementState contains the latest state of a management connection
|
||||||
|
message ManagementState {
|
||||||
|
string URL = 1;
|
||||||
|
bool connected = 2;
|
||||||
|
}
|
||||||
|
// FullStatus contains the full state held by the Status instance
|
||||||
|
message FullStatus {
|
||||||
|
ManagementState managementState = 1;
|
||||||
|
SignalState signalState = 2;
|
||||||
|
LocalPeerState localPeerState = 3;
|
||||||
|
repeated PeerState peers = 4;
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if ! which realpath > /dev/null 2>&1
|
||||||
|
then
|
||||||
|
echo realpath is not installed
|
||||||
|
echo run: brew install coreutils
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
old_pwd=$(pwd)
|
||||||
|
script_path=$(dirname $(realpath "$0"))
|
||||||
|
cd "$script_path"
|
||||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
|
||||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
|
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
|
||||||
protoc -I proto/ proto/daemon.proto --go_out=. --go-grpc_out=.
|
protoc -I ./ ./daemon.proto --go_out=../ --go-grpc_out=../
|
||||||
|
cd "$old_pwd"
|
||||||
@@ -3,6 +3,9 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -31,6 +34,8 @@ type Server struct {
|
|||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
config *internal.Config
|
config *internal.Config
|
||||||
proto.UnimplementedDaemonServiceServer
|
proto.UnimplementedDaemonServiceServer
|
||||||
|
|
||||||
|
statusRecorder *nbStatus.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
type oauthAuthFlow struct {
|
type oauthAuthFlow struct {
|
||||||
@@ -52,6 +57,8 @@ func New(ctx context.Context, managementURL, adminURL, configPath, logFile strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
state := internal.CtxGetState(s.rootCtx)
|
state := internal.CtxGetState(s.rootCtx)
|
||||||
|
|
||||||
// if current state contains any error, return it
|
// if current state contains any error, return it
|
||||||
@@ -86,11 +93,16 @@ func (s *Server) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if configuration exists, we just start connections.
|
// if configuration exists, we just start connections.
|
||||||
|
config, _ = internal.UpdateOldManagementPort(ctx, config, s.configPath)
|
||||||
|
|
||||||
s.config = config
|
s.config = config
|
||||||
|
|
||||||
|
if s.statusRecorder == nil {
|
||||||
|
s.statusRecorder = nbStatus.NewRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := internal.RunClient(ctx, config); err != nil {
|
if err := internal.RunClient(ctx, config, s.statusRecorder); err != nil {
|
||||||
log.Errorf("init connections: %v", err)
|
log.Errorf("init connections: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -158,6 +170,12 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg.ManagementUrl == "" {
|
||||||
|
config, _ = internal.UpdateOldManagementPort(ctx, config, s.configPath)
|
||||||
|
s.config = config
|
||||||
|
s.managementURL = config.ManagementURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
s.config = config
|
s.config = config
|
||||||
s.mutex.Unlock()
|
s.mutex.Unlock()
|
||||||
@@ -190,7 +208,8 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
|
|||||||
hostedClient := internal.NewHostedDeviceFlow(
|
hostedClient := internal.NewHostedDeviceFlow(
|
||||||
providerConfig.ProviderConfig.Audience,
|
providerConfig.ProviderConfig.Audience,
|
||||||
providerConfig.ProviderConfig.ClientID,
|
providerConfig.ProviderConfig.ClientID,
|
||||||
providerConfig.ProviderConfig.Domain,
|
providerConfig.ProviderConfig.TokenEndpoint,
|
||||||
|
providerConfig.ProviderConfig.DeviceAuthEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
if s.oauthAuthFlow.client != nil && s.oauthAuthFlow.client.GetClientID(ctx) == hostedClient.GetClientID(context.TODO()) {
|
if s.oauthAuthFlow.client != nil && s.oauthAuthFlow.client.GetClientID(ctx) == hostedClient.GetClientID(context.TODO()) {
|
||||||
@@ -303,6 +322,10 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
s.oauthAuthFlow.expiresAt = time.Now()
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
if loginStatus, err := s.loginAttempt(ctx, "", tokenInfo.AccessToken); err != nil {
|
if loginStatus, err := s.loginAttempt(ctx, "", tokenInfo.AccessToken); err != nil {
|
||||||
state.Set(loginStatus)
|
state.Set(loginStatus)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -346,8 +369,12 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
|
|||||||
return nil, fmt.Errorf("config is not defined, please call login command first")
|
return nil, fmt.Errorf("config is not defined, please call login command first")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.statusRecorder == nil {
|
||||||
|
s.statusRecorder = nbStatus.NewRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := internal.RunClient(ctx, s.config); err != nil {
|
if err := internal.RunClient(ctx, s.config, s.statusRecorder); err != nil {
|
||||||
log.Errorf("run client connection: %v", state.Wrap(err))
|
log.Errorf("run client connection: %v", state.Wrap(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -371,7 +398,7 @@ func (s *Server) Down(ctx context.Context, msg *proto.DownRequest) (*proto.DownR
|
|||||||
|
|
||||||
// Status starts engine work in the daemon.
|
// Status starts engine work in the daemon.
|
||||||
func (s *Server) Status(
|
func (s *Server) Status(
|
||||||
ctx context.Context,
|
_ context.Context,
|
||||||
msg *proto.StatusRequest,
|
msg *proto.StatusRequest,
|
||||||
) (*proto.StatusResponse, error) {
|
) (*proto.StatusResponse, error) {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
@@ -382,7 +409,19 @@ func (s *Server) Status(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &proto.StatusResponse{Status: string(status)}, nil
|
statusResponse := proto.StatusResponse{Status: string(status), DaemonVersion: system.NetbirdVersion()}
|
||||||
|
|
||||||
|
if s.statusRecorder == nil {
|
||||||
|
s.statusRecorder = nbStatus.NewRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.GetFullPeerStatus {
|
||||||
|
fullStatus := s.statusRecorder.GetFullStatus()
|
||||||
|
pbFullStatus := toProtoFullStatus(fullStatus)
|
||||||
|
statusResponse.FullStatus = pbFullStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
return &statusResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfig of the daemon.
|
// GetConfig of the daemon.
|
||||||
@@ -418,3 +457,39 @@ func (s *Server) GetConfig(ctx context.Context, msg *proto.GetConfigRequest) (*p
|
|||||||
PreSharedKey: preSharedKey,
|
PreSharedKey: preSharedKey,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toProtoFullStatus(fullStatus nbStatus.FullStatus) *proto.FullStatus {
|
||||||
|
pbFullStatus := proto.FullStatus{
|
||||||
|
ManagementState: &proto.ManagementState{},
|
||||||
|
SignalState: &proto.SignalState{},
|
||||||
|
LocalPeerState: &proto.LocalPeerState{},
|
||||||
|
Peers: []*proto.PeerState{},
|
||||||
|
}
|
||||||
|
|
||||||
|
pbFullStatus.ManagementState.URL = fullStatus.ManagementState.URL
|
||||||
|
pbFullStatus.ManagementState.Connected = fullStatus.ManagementState.Connected
|
||||||
|
|
||||||
|
pbFullStatus.SignalState.URL = fullStatus.SignalState.URL
|
||||||
|
pbFullStatus.SignalState.Connected = fullStatus.SignalState.Connected
|
||||||
|
|
||||||
|
pbFullStatus.LocalPeerState.IP = fullStatus.LocalPeerState.IP
|
||||||
|
pbFullStatus.LocalPeerState.PubKey = fullStatus.LocalPeerState.PubKey
|
||||||
|
pbFullStatus.LocalPeerState.KernelInterface = fullStatus.LocalPeerState.KernelInterface
|
||||||
|
pbFullStatus.LocalPeerState.Fqdn = fullStatus.LocalPeerState.FQDN
|
||||||
|
|
||||||
|
for _, peerState := range fullStatus.Peers {
|
||||||
|
pbPeerState := &proto.PeerState{
|
||||||
|
IP: peerState.IP,
|
||||||
|
PubKey: peerState.PubKey,
|
||||||
|
ConnStatus: peerState.ConnStatus,
|
||||||
|
ConnStatusUpdate: timestamppb.New(peerState.ConnStatusUpdate),
|
||||||
|
Relayed: peerState.Relayed,
|
||||||
|
Direct: peerState.Direct,
|
||||||
|
LocalIceCandidateType: peerState.LocalIceCandidateType,
|
||||||
|
RemoteIceCandidateType: peerState.RemoteIceCandidateType,
|
||||||
|
Fqdn: peerState.FQDN,
|
||||||
|
}
|
||||||
|
pbFullStatus.Peers = append(pbFullStatus.Peers, pbPeerState)
|
||||||
|
}
|
||||||
|
return &pbFullStatus
|
||||||
|
}
|
||||||
|
|||||||
116
client/ssh/client.go
Normal file
116
client/ssh/client.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/term"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client wraps crypto/ssh Client to simplify usage
|
||||||
|
type Client struct {
|
||||||
|
client *ssh.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the wrapped SSH Client
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
return c.client.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenTerminal starts an interactive terminal session with the remote SSH server
|
||||||
|
func (c *Client) OpenTerminal() error {
|
||||||
|
session, err := c.client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open new session: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := session.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fd := int(os.Stdout.Fd())
|
||||||
|
state, err := term.MakeRaw(fd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run raw terminal: %s", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := term.Restore(fd, state)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
w, h, err := term.GetSize(fd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("terminal get size: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
modes := ssh.TerminalModes{
|
||||||
|
ssh.ECHO: 1,
|
||||||
|
ssh.TTY_OP_ISPEED: 14400,
|
||||||
|
ssh.TTY_OP_OSPEED: 14400,
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal := os.Getenv("TERM")
|
||||||
|
if terminal == "" {
|
||||||
|
terminal = "xterm-256color"
|
||||||
|
}
|
||||||
|
if err := session.RequestPty(terminal, h, w, modes); err != nil {
|
||||||
|
return fmt.Errorf("failed requesting pty session with xterm: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Stdout = os.Stdout
|
||||||
|
session.Stderr = os.Stderr
|
||||||
|
session.Stdin = os.Stdin
|
||||||
|
|
||||||
|
if err := session.Shell(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start login shell on the remote host: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := session.Wait(); err != nil {
|
||||||
|
if e, ok := err.(*ssh.ExitError); ok {
|
||||||
|
switch e.ExitStatus() {
|
||||||
|
case 130:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed running SSH session: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialWithKey connects to the remote SSH server with a provided private key file (PEM).
|
||||||
|
func DialWithKey(addr, user string, privateKey []byte) (*Client, error) {
|
||||||
|
|
||||||
|
signer, err := ssh.ParsePrivateKey(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &ssh.ClientConfig{
|
||||||
|
User: user,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
Auth: []ssh.AuthMethod{
|
||||||
|
ssh.PublicKeys(signer),
|
||||||
|
},
|
||||||
|
HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }),
|
||||||
|
}
|
||||||
|
|
||||||
|
return Dial("tcp", addr, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial connects to the remote SSH server.
|
||||||
|
func Dial(network, addr string, config *ssh.ClientConfig) (*Client, error) {
|
||||||
|
client, err := ssh.Dial(network, addr, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
36
client/ssh/login.go
Normal file
36
client/ssh/login.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getLoginCmd(user string, remoteAddr net.Addr) (loginPath string, args []string, err error) {
|
||||||
|
loginPath, err = exec.LookPath("login")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrPort, err := netip.ParseAddrPort(remoteAddr.String())
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
|
||||||
|
if util.FileExists("/etc/arch-release") && !util.FileExists("/etc/pam.d/remote") {
|
||||||
|
// detect if Arch Linux
|
||||||
|
return loginPath, []string{"-f", user, "-p"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginPath, []string{"-f", user, "-h", addrPort.Addr().String(), "-p"}, nil
|
||||||
|
} else if runtime.GOOS == "darwin" {
|
||||||
|
return loginPath, []string{"-fp", "-h", addrPort.Addr().String(), user}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil, fmt.Errorf("unsupported platform")
|
||||||
|
}
|
||||||
10
client/ssh/lookup.go
Normal file
10
client/ssh/lookup.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//go:build !darwin
|
||||||
|
// +build !darwin
|
||||||
|
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import "os/user"
|
||||||
|
|
||||||
|
func userNameLookup(username string) (*user.User, error) {
|
||||||
|
return user.Lookup(username)
|
||||||
|
}
|
||||||
47
client/ssh/lookup_darwin.go
Normal file
47
client/ssh/lookup_darwin.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
//go:build darwin
|
||||||
|
// +build darwin
|
||||||
|
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func userNameLookup(username string) (*user.User, error) {
|
||||||
|
var userObject *user.User
|
||||||
|
userObject, err := user.Lookup(username)
|
||||||
|
if err != nil && err.Error() == user.UnknownUserError(username).Error() {
|
||||||
|
return idUserNameLookup(username)
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userObject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func idUserNameLookup(username string) (*user.User, error) {
|
||||||
|
cmd := exec.Command("id", "-P", username)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error while retrieving user with id -P command, error: %v", err)
|
||||||
|
}
|
||||||
|
colon := ":"
|
||||||
|
|
||||||
|
if !bytes.Contains(out, []byte(username+colon)) {
|
||||||
|
return nil, fmt.Errorf("unable to find user in returned string")
|
||||||
|
}
|
||||||
|
// netbird:********:501:20::0:0:netbird:/Users/netbird:/bin/zsh
|
||||||
|
parts := strings.SplitN(string(out), colon, 10)
|
||||||
|
userObject := &user.User{
|
||||||
|
Username: parts[0],
|
||||||
|
Uid: parts[2],
|
||||||
|
Gid: parts[3],
|
||||||
|
Name: parts[7],
|
||||||
|
HomeDir: parts[8],
|
||||||
|
}
|
||||||
|
return userObject, nil
|
||||||
|
}
|
||||||
250
client/ssh/server.go
Normal file
250
client/ssh/server.go
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/creack/pty"
|
||||||
|
"github.com/gliderlabs/ssh"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultSSHPort is the default SSH port of the NetBird's embedded SSH server
|
||||||
|
const DefaultSSHPort = 44338
|
||||||
|
|
||||||
|
// DefaultSSHServer is a function that creates DefaultServer
|
||||||
|
func DefaultSSHServer(hostKeyPEM []byte, addr string) (Server, error) {
|
||||||
|
return newDefaultServer(hostKeyPEM, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server is an interface of SSH server
|
||||||
|
type Server interface {
|
||||||
|
// Stop stops SSH server.
|
||||||
|
Stop() error
|
||||||
|
// Start starts SSH server. Blocking
|
||||||
|
Start() error
|
||||||
|
// RemoveAuthorizedKey removes SSH key of a given peer from the authorized keys
|
||||||
|
RemoveAuthorizedKey(peer string)
|
||||||
|
// AddAuthorizedKey add a given peer key to server authorized keys
|
||||||
|
AddAuthorizedKey(peer, newKey string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultServer is the embedded NetBird SSH server
|
||||||
|
type DefaultServer struct {
|
||||||
|
listener net.Listener
|
||||||
|
// authorizedKeys is ssh pub key indexed by peer WireGuard public key
|
||||||
|
authorizedKeys map[string]ssh.PublicKey
|
||||||
|
mu sync.Mutex
|
||||||
|
hostKeyPEM []byte
|
||||||
|
sessions []ssh.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDefaultServer creates new server with provided host key
|
||||||
|
func newDefaultServer(hostKeyPEM []byte, addr string) (*DefaultServer, error) {
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
allowedKeys := make(map[string]ssh.PublicKey)
|
||||||
|
return &DefaultServer{listener: ln, mu: sync.Mutex{}, hostKeyPEM: hostKeyPEM, authorizedKeys: allowedKeys, sessions: make([]ssh.Session, 0)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAuthorizedKey removes SSH key of a given peer from the authorized keys
|
||||||
|
func (srv *DefaultServer) RemoveAuthorizedKey(peer string) {
|
||||||
|
srv.mu.Lock()
|
||||||
|
defer srv.mu.Unlock()
|
||||||
|
|
||||||
|
delete(srv.authorizedKeys, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAuthorizedKey add a given peer key to server authorized keys
|
||||||
|
func (srv *DefaultServer) AddAuthorizedKey(peer, newKey string) error {
|
||||||
|
srv.mu.Lock()
|
||||||
|
defer srv.mu.Unlock()
|
||||||
|
|
||||||
|
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(newKey))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.authorizedKeys[peer] = parsedKey
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops SSH server.
|
||||||
|
func (srv *DefaultServer) Stop() error {
|
||||||
|
srv.mu.Lock()
|
||||||
|
defer srv.mu.Unlock()
|
||||||
|
err := srv.listener.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, session := range srv.sessions {
|
||||||
|
err := session.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed closing SSH session from %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *DefaultServer) publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||||
|
srv.mu.Lock()
|
||||||
|
defer srv.mu.Unlock()
|
||||||
|
|
||||||
|
for _, allowed := range srv.authorizedKeys {
|
||||||
|
if ssh.KeysEqual(allowed, key) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareUserEnv(user *user.User, shell string) []string {
|
||||||
|
return []string{
|
||||||
|
fmt.Sprintf("SHELL=" + shell),
|
||||||
|
fmt.Sprintf("USER=" + user.Username),
|
||||||
|
fmt.Sprintf("HOME=" + user.HomeDir),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptEnv(s string) bool {
|
||||||
|
split := strings.Split(s, "=")
|
||||||
|
if len(split) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return split[0] == "TERM" || split[0] == "LANG" || strings.HasPrefix(split[0], "LC_")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sessionHandler handles SSH session post auth
|
||||||
|
func (srv *DefaultServer) sessionHandler(session ssh.Session) {
|
||||||
|
srv.mu.Lock()
|
||||||
|
srv.sessions = append(srv.sessions, session)
|
||||||
|
srv.mu.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := session.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
localUser, err := userNameLookup(session.User())
|
||||||
|
if err != nil {
|
||||||
|
_, err = fmt.Fprintf(session, "remote SSH server couldn't find local user %s\n", session.User()) //nolint
|
||||||
|
err = session.Exit(1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Warnf("failed SSH session from %v, user %s", session.RemoteAddr(), session.User())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ptyReq, winCh, isPty := session.Pty()
|
||||||
|
if isPty {
|
||||||
|
loginCmd, loginArgs, err := getLoginCmd(localUser.Username, session.RemoteAddr())
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed logging-in user %s from remote IP %s", localUser.Username, session.RemoteAddr().String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd := exec.Command(loginCmd, loginArgs...)
|
||||||
|
go func() {
|
||||||
|
<-session.Context().Done()
|
||||||
|
err := cmd.Process.Kill()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
cmd.Dir = localUser.HomeDir
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
|
||||||
|
cmd.Env = append(cmd.Env, prepareUserEnv(localUser, getUserShell(localUser.Uid))...)
|
||||||
|
for _, v := range session.Environ() {
|
||||||
|
if acceptEnv(v) {
|
||||||
|
cmd.Env = append(cmd.Env, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := pty.Start(cmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed starting SSH server %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for win := range winCh {
|
||||||
|
setWinSize(file, win.Width, win.Height)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
srv.stdInOut(file, session)
|
||||||
|
|
||||||
|
err = cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err := io.WriteString(session, "only PTY is supported.\n")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = session.Exit(1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *DefaultServer) stdInOut(file *os.File, session ssh.Session) {
|
||||||
|
go func() {
|
||||||
|
// stdin
|
||||||
|
_, err := io.Copy(file, session)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// stdout
|
||||||
|
_, err := io.Copy(session, file)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts SSH server. Blocking
|
||||||
|
func (srv *DefaultServer) Start() error {
|
||||||
|
log.Infof("starting SSH server on addr: %s", srv.listener.Addr().String())
|
||||||
|
|
||||||
|
publicKeyOption := ssh.PublicKeyAuth(srv.publicKeyHandler)
|
||||||
|
hostKeyPEM := ssh.HostKeyPEM(srv.hostKeyPEM)
|
||||||
|
err := ssh.Serve(srv.listener, srv.sessionHandler, publicKeyOption, hostKeyPEM)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserShell(userID string) string {
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
output, _ := exec.Command("getent", "passwd", userID).Output()
|
||||||
|
line := strings.SplitN(string(output), ":", 10)
|
||||||
|
if len(line) > 6 {
|
||||||
|
return strings.TrimSpace(line[6])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shell := os.Getenv("SHELL")
|
||||||
|
if shell == "" {
|
||||||
|
shell = "/bin/sh"
|
||||||
|
}
|
||||||
|
return shell
|
||||||
|
}
|
||||||
44
client/ssh/server_mock.go
Normal file
44
client/ssh/server_mock.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// MockServer mocks ssh.Server
|
||||||
|
type MockServer struct {
|
||||||
|
Ctx context.Context
|
||||||
|
StopFunc func() error
|
||||||
|
StartFunc func() error
|
||||||
|
AddAuthorizedKeyFunc func(peer, newKey string) error
|
||||||
|
RemoveAuthorizedKeyFunc func(peer string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAuthorizedKey removes SSH key of a given peer from the authorized keys
|
||||||
|
func (srv *MockServer) RemoveAuthorizedKey(peer string) {
|
||||||
|
if srv.RemoveAuthorizedKeyFunc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
srv.RemoveAuthorizedKeyFunc(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAuthorizedKey add a given peer key to server authorized keys
|
||||||
|
func (srv *MockServer) AddAuthorizedKey(peer, newKey string) error {
|
||||||
|
if srv.AddAuthorizedKeyFunc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return srv.AddAuthorizedKeyFunc(peer, newKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops SSH server.
|
||||||
|
func (srv *MockServer) Stop() error {
|
||||||
|
if srv.StopFunc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return srv.StopFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts SSH server. Blocking
|
||||||
|
func (srv *MockServer) Start() error {
|
||||||
|
if srv.StartFunc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return srv.StartFunc()
|
||||||
|
}
|
||||||
121
client/ssh/server_test.go
Normal file
121
client/ssh/server_test.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServer_AddAuthorizedKey(t *testing.T) {
|
||||||
|
key, err := GeneratePrivateKey(ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server, err := newDefaultServer(key, "localhost:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add multiple keys
|
||||||
|
keys := map[string][]byte{}
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
peer := fmt.Sprintf("%s-%d", "remotePeer", i)
|
||||||
|
remotePrivKey, err := GeneratePrivateKey(ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
remotePubKey, err := GeneratePublicKey(remotePrivKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.AddAuthorizedKey(peer, string(remotePubKey))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
keys[peer] = remotePubKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that all keys have been added
|
||||||
|
for peer, remotePubKey := range keys {
|
||||||
|
k, ok := server.authorizedKeys[peer]
|
||||||
|
assert.True(t, ok, "expecting remotePeer key to be found in authorizedKeys")
|
||||||
|
|
||||||
|
assert.Equal(t, string(remotePubKey), strings.TrimSpace(string(ssh.MarshalAuthorizedKey(k))))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_RemoveAuthorizedKey(t *testing.T) {
|
||||||
|
key, err := GeneratePrivateKey(ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server, err := newDefaultServer(key, "localhost:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remotePrivKey, err := GeneratePrivateKey(ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
remotePubKey, err := GeneratePublicKey(remotePrivKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.AddAuthorizedKey("remotePeer", string(remotePubKey))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server.RemoveAuthorizedKey("remotePeer")
|
||||||
|
|
||||||
|
_, ok := server.authorizedKeys["remotePeer"]
|
||||||
|
assert.False(t, ok, "expecting remotePeer's SSH key to be removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_PubKeyHandler(t *testing.T) {
|
||||||
|
key, err := GeneratePrivateKey(ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server, err := newDefaultServer(key, "localhost:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys []ssh.PublicKey
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
peer := fmt.Sprintf("%s-%d", "remotePeer", i)
|
||||||
|
remotePrivKey, err := GeneratePrivateKey(ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
remotePubKey, err := GeneratePublicKey(remotePrivKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteParsedPubKey, _, _, _, err := ssh.ParseAuthorizedKey(remotePubKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.AddAuthorizedKey(peer, string(remotePubKey))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
keys = append(keys, remoteParsedPubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
accepted := server.publicKeyHandler(nil, key)
|
||||||
|
|
||||||
|
assert.Truef(t, accepted, "expecting SSH connection to be accepted for a given SSH key %s", string(ssh.MarshalAuthorizedKey(key)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
86
client/ssh/util.go
Normal file
86
client/ssh/util.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyType is a type of SSH key
|
||||||
|
type KeyType string
|
||||||
|
|
||||||
|
// ED25519 is key of type ed25519
|
||||||
|
const ED25519 KeyType = "ed25519"
|
||||||
|
|
||||||
|
// ECDSA is key of type ecdsa
|
||||||
|
const ECDSA KeyType = "ecdsa"
|
||||||
|
|
||||||
|
// RSA is key of type rsa
|
||||||
|
const RSA KeyType = "rsa"
|
||||||
|
|
||||||
|
// RSAKeySize is a size of newly generated RSA key
|
||||||
|
const RSAKeySize = 2048
|
||||||
|
|
||||||
|
// GeneratePrivateKey creates RSA Private Key of specified byte size
|
||||||
|
func GeneratePrivateKey(keyType KeyType) ([]byte, error) {
|
||||||
|
|
||||||
|
var key crypto.Signer
|
||||||
|
var err error
|
||||||
|
switch keyType {
|
||||||
|
case ED25519:
|
||||||
|
_, key, err = ed25519.GenerateKey(rand.Reader)
|
||||||
|
case ECDSA:
|
||||||
|
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
case RSA:
|
||||||
|
key, err = rsa.GenerateKey(rand.Reader, RSAKeySize)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported ket type %s", keyType)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pemBytes, err := EncodePrivateKeyToPEM(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pemBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePublicKey returns the public part of the private key
|
||||||
|
func GeneratePublicKey(key []byte) ([]byte, error) {
|
||||||
|
signer, err := gossh.ParsePrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
strKey := strings.TrimSpace(string(gossh.MarshalAuthorizedKey(signer.PublicKey())))
|
||||||
|
return []byte(strKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodePrivateKeyToPEM encodes Private Key from RSA to PEM format
|
||||||
|
func EncodePrivateKeyToPEM(privateKey crypto.Signer) ([]byte, error) {
|
||||||
|
mk, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// pem.Block
|
||||||
|
privBlock := pem.Block{
|
||||||
|
Type: "PRIVATE KEY",
|
||||||
|
Bytes: mk,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private key in PEM format
|
||||||
|
privatePEM := pem.EncodeToMemory(&privBlock)
|
||||||
|
return privatePEM, nil
|
||||||
|
}
|
||||||
14
client/ssh/window_unix.go
Normal file
14
client/ssh/window_unix.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build linux || darwin
|
||||||
|
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setWinSize(file *os.File, width, height int) {
|
||||||
|
syscall.Syscall(syscall.SYS_IOCTL, file.Fd(), uintptr(syscall.TIOCSWINSZ), //nolint
|
||||||
|
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(height), uint16(width), 0, 0})))
|
||||||
|
}
|
||||||
9
client/ssh/window_windows.go
Normal file
9
client/ssh/window_windows.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setWinSize(file *os.File, width, height int) {
|
||||||
|
|
||||||
|
}
|
||||||
241
client/status/status.go
Normal file
241
client/status/status.go
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PeerState contains the latest state of a peer
|
||||||
|
type PeerState struct {
|
||||||
|
IP string
|
||||||
|
PubKey string
|
||||||
|
FQDN string
|
||||||
|
ConnStatus string
|
||||||
|
ConnStatusUpdate time.Time
|
||||||
|
Relayed bool
|
||||||
|
Direct bool
|
||||||
|
LocalIceCandidateType string
|
||||||
|
RemoteIceCandidateType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalPeerState contains the latest state of the local peer
|
||||||
|
type LocalPeerState struct {
|
||||||
|
IP string
|
||||||
|
PubKey string
|
||||||
|
KernelInterface bool
|
||||||
|
FQDN string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignalState contains the latest state of a signal connection
|
||||||
|
type SignalState struct {
|
||||||
|
URL string
|
||||||
|
Connected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagementState contains the latest state of a management connection
|
||||||
|
type ManagementState struct {
|
||||||
|
URL string
|
||||||
|
Connected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FullStatus contains the full state held by the Status instance
|
||||||
|
type FullStatus struct {
|
||||||
|
Peers []PeerState
|
||||||
|
ManagementState ManagementState
|
||||||
|
SignalState SignalState
|
||||||
|
LocalPeerState LocalPeerState
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status holds a state of peers, signal and management connections
|
||||||
|
type Status struct {
|
||||||
|
mux sync.Mutex
|
||||||
|
peers map[string]PeerState
|
||||||
|
changeNotify map[string]chan struct{}
|
||||||
|
signal SignalState
|
||||||
|
management ManagementState
|
||||||
|
localPeer LocalPeerState
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRecorder returns a new Status instance
|
||||||
|
func NewRecorder() *Status {
|
||||||
|
return &Status{
|
||||||
|
peers: make(map[string]PeerState),
|
||||||
|
changeNotify: make(map[string]chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPeer adds peer to Daemon status map
|
||||||
|
func (d *Status) AddPeer(peerPubKey string) error {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
_, ok := d.peers[peerPubKey]
|
||||||
|
if ok {
|
||||||
|
return errors.New("peer already exist")
|
||||||
|
}
|
||||||
|
d.peers[peerPubKey] = PeerState{PubKey: peerPubKey}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeer adds peer to Daemon status map
|
||||||
|
func (d *Status) GetPeer(peerPubKey string) (PeerState, error) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
state, ok := d.peers[peerPubKey]
|
||||||
|
if !ok {
|
||||||
|
return PeerState{}, errors.New("peer not found")
|
||||||
|
}
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemovePeer removes peer from Daemon status map
|
||||||
|
func (d *Status) RemovePeer(peerPubKey string) error {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
_, ok := d.peers[peerPubKey]
|
||||||
|
if ok {
|
||||||
|
delete(d.peers, peerPubKey)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("no peer with to remove")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePeerState updates peer status
|
||||||
|
func (d *Status) UpdatePeerState(receivedState PeerState) error {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
peerState, ok := d.peers[receivedState.PubKey]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("peer doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if receivedState.IP != "" {
|
||||||
|
peerState.IP = receivedState.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
if receivedState.ConnStatus != peerState.ConnStatus {
|
||||||
|
peerState.ConnStatus = receivedState.ConnStatus
|
||||||
|
peerState.ConnStatusUpdate = receivedState.ConnStatusUpdate
|
||||||
|
peerState.Direct = receivedState.Direct
|
||||||
|
peerState.Relayed = receivedState.Relayed
|
||||||
|
peerState.LocalIceCandidateType = receivedState.LocalIceCandidateType
|
||||||
|
peerState.RemoteIceCandidateType = receivedState.RemoteIceCandidateType
|
||||||
|
}
|
||||||
|
|
||||||
|
d.peers[receivedState.PubKey] = peerState
|
||||||
|
|
||||||
|
ch, found := d.changeNotify[receivedState.PubKey]
|
||||||
|
if found && ch != nil {
|
||||||
|
close(ch)
|
||||||
|
d.changeNotify[receivedState.PubKey] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePeerFQDN update peer's state fqdn only
|
||||||
|
func (d *Status) UpdatePeerFQDN(peerPubKey, fqdn string) error {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
peerState, ok := d.peers[peerPubKey]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("peer doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
peerState.FQDN = fqdn
|
||||||
|
d.peers[peerPubKey] = peerState
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeerStateChangeNotifier returns a change notifier channel for a peer
|
||||||
|
func (d *Status) GetPeerStateChangeNotifier(peer string) <-chan struct{} {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
ch, found := d.changeNotify[peer]
|
||||||
|
if !found || ch == nil {
|
||||||
|
ch = make(chan struct{})
|
||||||
|
d.changeNotify[peer] = ch
|
||||||
|
}
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLocalPeerState updates local peer status
|
||||||
|
func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
d.localPeer = localPeerState
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanLocalPeerState cleans local peer status
|
||||||
|
func (d *Status) CleanLocalPeerState() {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
d.localPeer = LocalPeerState{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkManagementDisconnected sets ManagementState to disconnected
|
||||||
|
func (d *Status) MarkManagementDisconnected(managementURL string) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
d.management = ManagementState{
|
||||||
|
URL: managementURL,
|
||||||
|
Connected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkManagementConnected sets ManagementState to connected
|
||||||
|
func (d *Status) MarkManagementConnected(managementURL string) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
d.management = ManagementState{
|
||||||
|
URL: managementURL,
|
||||||
|
Connected: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkSignalDisconnected sets SignalState to disconnected
|
||||||
|
func (d *Status) MarkSignalDisconnected(signalURL string) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
d.signal = SignalState{
|
||||||
|
signalURL,
|
||||||
|
false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkSignalConnected sets SignalState to connected
|
||||||
|
func (d *Status) MarkSignalConnected(signalURL string) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
d.signal = SignalState{
|
||||||
|
signalURL,
|
||||||
|
true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFullStatus gets full status
|
||||||
|
func (d *Status) GetFullStatus() FullStatus {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
fullStatus := FullStatus{
|
||||||
|
ManagementState: d.management,
|
||||||
|
SignalState: d.signal,
|
||||||
|
LocalPeerState: d.localPeer,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, status := range d.peers {
|
||||||
|
fullStatus.Peers = append(fullStatus.Peers, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullStatus
|
||||||
|
}
|
||||||
243
client/status/status_test.go
Normal file
243
client/status/status_test.go
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddPeer(t *testing.T) {
|
||||||
|
key := "abc"
|
||||||
|
status := NewRecorder()
|
||||||
|
err := status.AddPeer(key)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
_, exists := status.peers[key]
|
||||||
|
assert.True(t, exists, "value was found")
|
||||||
|
|
||||||
|
err = status.AddPeer(key)
|
||||||
|
|
||||||
|
assert.Error(t, err, "should return error on duplicate")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPeer(t *testing.T) {
|
||||||
|
key := "abc"
|
||||||
|
status := NewRecorder()
|
||||||
|
err := status.AddPeer(key)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
peerStatus, err := status.GetPeer(key)
|
||||||
|
assert.NoError(t, err, "shouldn't return error on getting peer")
|
||||||
|
|
||||||
|
assert.Equal(t, key, peerStatus.PubKey, "retrieved public key should match")
|
||||||
|
|
||||||
|
_, err = status.GetPeer("non_existing_key")
|
||||||
|
assert.Error(t, err, "should return error when peer doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdatePeerState(t *testing.T) {
|
||||||
|
key := "abc"
|
||||||
|
ip := "10.10.10.10"
|
||||||
|
status := NewRecorder()
|
||||||
|
peerState := PeerState{
|
||||||
|
PubKey: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
status.peers[key] = peerState
|
||||||
|
|
||||||
|
peerState.IP = ip
|
||||||
|
|
||||||
|
err := status.UpdatePeerState(peerState)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
state, exists := status.peers[key]
|
||||||
|
assert.True(t, exists, "state should be found")
|
||||||
|
assert.Equal(t, ip, state.IP, "ip should be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatus_UpdatePeerFQDN(t *testing.T) {
|
||||||
|
key := "abc"
|
||||||
|
fqdn := "peer-a.netbird.local"
|
||||||
|
status := NewRecorder()
|
||||||
|
peerState := PeerState{
|
||||||
|
PubKey: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
status.peers[key] = peerState
|
||||||
|
|
||||||
|
err := status.UpdatePeerFQDN(key, fqdn)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
state, exists := status.peers[key]
|
||||||
|
assert.True(t, exists, "state should be found")
|
||||||
|
assert.Equal(t, fqdn, state.FQDN, "fqdn should be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPeerStateChangeNotifierLogic(t *testing.T) {
|
||||||
|
key := "abc"
|
||||||
|
ip := "10.10.10.10"
|
||||||
|
status := NewRecorder()
|
||||||
|
peerState := PeerState{
|
||||||
|
PubKey: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
status.peers[key] = peerState
|
||||||
|
|
||||||
|
ch := status.GetPeerStateChangeNotifier(key)
|
||||||
|
assert.NotNil(t, ch, "channel shouldn't be nil")
|
||||||
|
|
||||||
|
peerState.IP = ip
|
||||||
|
|
||||||
|
err := status.UpdatePeerState(peerState)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
default:
|
||||||
|
t.Errorf("channel wasn't closed after update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemovePeer(t *testing.T) {
|
||||||
|
key := "abc"
|
||||||
|
status := NewRecorder()
|
||||||
|
peerState := PeerState{
|
||||||
|
PubKey: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
status.peers[key] = peerState
|
||||||
|
|
||||||
|
err := status.RemovePeer(key)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
_, exists := status.peers[key]
|
||||||
|
assert.False(t, exists, "state value shouldn't be found")
|
||||||
|
|
||||||
|
err = status.RemovePeer("not existing")
|
||||||
|
assert.Error(t, err, "should return error when peer doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateLocalPeerState(t *testing.T) {
|
||||||
|
localPeerState := LocalPeerState{
|
||||||
|
IP: "10.10.10.10",
|
||||||
|
PubKey: "abc",
|
||||||
|
KernelInterface: false,
|
||||||
|
}
|
||||||
|
status := NewRecorder()
|
||||||
|
|
||||||
|
status.UpdateLocalPeerState(localPeerState)
|
||||||
|
|
||||||
|
assert.Equal(t, localPeerState, status.localPeer, "local peer status should be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanLocalPeerState(t *testing.T) {
|
||||||
|
emptyLocalPeerState := LocalPeerState{}
|
||||||
|
localPeerState := LocalPeerState{
|
||||||
|
IP: "10.10.10.10",
|
||||||
|
PubKey: "abc",
|
||||||
|
KernelInterface: false,
|
||||||
|
}
|
||||||
|
status := NewRecorder()
|
||||||
|
|
||||||
|
status.localPeer = localPeerState
|
||||||
|
|
||||||
|
status.CleanLocalPeerState()
|
||||||
|
|
||||||
|
assert.Equal(t, emptyLocalPeerState, status.localPeer, "local peer status should be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSignalState(t *testing.T) {
|
||||||
|
url := "https://signal"
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
connected bool
|
||||||
|
want SignalState
|
||||||
|
}{
|
||||||
|
{"should mark as connected", true, SignalState{
|
||||||
|
|
||||||
|
URL: url,
|
||||||
|
Connected: true,
|
||||||
|
}},
|
||||||
|
{"should mark as disconnected", false, SignalState{
|
||||||
|
URL: url,
|
||||||
|
Connected: false,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
status := NewRecorder()
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if test.connected {
|
||||||
|
status.MarkSignalConnected(url)
|
||||||
|
} else {
|
||||||
|
status.MarkSignalDisconnected(url)
|
||||||
|
}
|
||||||
|
assert.Equal(t, test.want, status.signal, "signal status should be equal")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateManagementState(t *testing.T) {
|
||||||
|
url := "https://management"
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
connected bool
|
||||||
|
want ManagementState
|
||||||
|
}{
|
||||||
|
{"should mark as connected", true, ManagementState{
|
||||||
|
|
||||||
|
URL: url,
|
||||||
|
Connected: true,
|
||||||
|
}},
|
||||||
|
{"should mark as disconnected", false, ManagementState{
|
||||||
|
URL: url,
|
||||||
|
Connected: false,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
status := NewRecorder()
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if test.connected {
|
||||||
|
status.MarkManagementConnected(url)
|
||||||
|
} else {
|
||||||
|
status.MarkManagementDisconnected(url)
|
||||||
|
}
|
||||||
|
assert.Equal(t, test.want, status.management, "signal status should be equal")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFullStatus(t *testing.T) {
|
||||||
|
key1 := "abc"
|
||||||
|
key2 := "def"
|
||||||
|
managementState := ManagementState{
|
||||||
|
URL: "https://signal",
|
||||||
|
Connected: true,
|
||||||
|
}
|
||||||
|
signalState := SignalState{
|
||||||
|
URL: "https://signal",
|
||||||
|
Connected: true,
|
||||||
|
}
|
||||||
|
peerState1 := PeerState{
|
||||||
|
PubKey: key1,
|
||||||
|
}
|
||||||
|
|
||||||
|
peerState2 := PeerState{
|
||||||
|
PubKey: key2,
|
||||||
|
}
|
||||||
|
|
||||||
|
status := NewRecorder()
|
||||||
|
|
||||||
|
status.management = managementState
|
||||||
|
status.signal = signalState
|
||||||
|
status.peers[key1] = peerState1
|
||||||
|
status.peers[key2] = peerState2
|
||||||
|
|
||||||
|
fullStatus := status.GetFullStatus()
|
||||||
|
|
||||||
|
assert.Equal(t, managementState, fullStatus.ManagementState, "management status should be equal")
|
||||||
|
assert.Equal(t, signalState, fullStatus.SignalState, "signal status should be equal")
|
||||||
|
assert.ElementsMatch(t, []PeerState{peerState1, peerState2}, fullStatus.Peers, "peers states should match")
|
||||||
|
}
|
||||||
@@ -4,41 +4,25 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetInfo retrieves and parses the system information
|
// GetInfo retrieves and parses the system information
|
||||||
func GetInfo(ctx context.Context) *Info {
|
func GetInfo(ctx context.Context) *Info {
|
||||||
out := _getInfo()
|
utsname := unix.Utsname{}
|
||||||
for strings.Contains(out, "broken pipe") {
|
err := unix.Uname(&utsname)
|
||||||
out = _getInfo()
|
if err != nil {
|
||||||
time.Sleep(500 * time.Millisecond)
|
fmt.Println("getInfo:", err)
|
||||||
}
|
}
|
||||||
osStr := strings.Replace(out, "\n", "", -1)
|
sysName := string(bytes.Split(utsname.Sysname[:], []byte{0})[0])
|
||||||
osStr = strings.Replace(osStr, "\r\n", "", -1)
|
machine := string(bytes.Split(utsname.Machine[:], []byte{0})[0])
|
||||||
osInfo := strings.Split(osStr, " ")
|
release := string(bytes.Split(utsname.Release[:], []byte{0})[0])
|
||||||
gio := &Info{Kernel: osInfo[0], OSVersion: osInfo[1], Core: osInfo[1], Platform: osInfo[2], OS: osInfo[0], GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
|
gio := &Info{Kernel: sysName, OSVersion: release, Core: release, Platform: machine, OS: sysName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
|
||||||
gio.Hostname, _ = os.Hostname()
|
gio.Hostname, _ = os.Hostname()
|
||||||
gio.WiretrusteeVersion = NetbirdVersion()
|
gio.WiretrusteeVersion = NetbirdVersion()
|
||||||
gio.UIVersion = extractUserAgent(ctx)
|
gio.UIVersion = extractUserAgent(ctx)
|
||||||
|
|
||||||
return gio
|
return gio
|
||||||
}
|
}
|
||||||
|
|
||||||
func _getInfo() string {
|
|
||||||
cmd := exec.Command("uname", "-srm")
|
|
||||||
cmd.Stdin = strings.NewReader("some input")
|
|
||||||
var out bytes.Buffer
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("getInfo:", err)
|
|
||||||
}
|
|
||||||
return out.String()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,36 +1,17 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetInfo retrieves and parses the system information
|
// GetInfo retrieves and parses the system information
|
||||||
func GetInfo(ctx context.Context) *Info {
|
func GetInfo(ctx context.Context) *Info {
|
||||||
cmd := exec.Command("cmd", "ver")
|
ver := getOSVersion()
|
||||||
cmd.Stdin = strings.NewReader("some")
|
|
||||||
var out bytes.Buffer
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
osStr := strings.Replace(out.String(), "\n", "", -1)
|
|
||||||
osStr = strings.Replace(osStr, "\r\n", "", -1)
|
|
||||||
tmp1 := strings.Index(osStr, "[Version")
|
|
||||||
tmp2 := strings.Index(osStr, "]")
|
|
||||||
var ver string
|
|
||||||
if tmp1 == -1 || tmp2 == -1 {
|
|
||||||
ver = "unknown"
|
|
||||||
} else {
|
|
||||||
ver = osStr[tmp1+9 : tmp2]
|
|
||||||
}
|
|
||||||
gio := &Info{Kernel: "windows", OSVersion: ver, Core: ver, Platform: "unknown", OS: "windows", GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
|
gio := &Info{Kernel: "windows", OSVersion: ver, Core: ver, Platform: "unknown", OS: "windows", GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
|
||||||
gio.Hostname, _ = os.Hostname()
|
gio.Hostname, _ = os.Hostname()
|
||||||
gio.WiretrusteeVersion = NetbirdVersion()
|
gio.WiretrusteeVersion = NetbirdVersion()
|
||||||
@@ -38,3 +19,37 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
|
|
||||||
return gio
|
return gio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getOSVersion() string {
|
||||||
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return "0.0.0.0"
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
deferErr := k.Close()
|
||||||
|
if deferErr != nil {
|
||||||
|
log.Error(deferErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
major, _, err := k.GetIntegerValue("CurrentMajorVersionNumber")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
minor, _, err := k.GetIntegerValue("CurrentMinorVersionNumber")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
build, _, err := k.GetStringValue("CurrentBuildNumber")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
// Update Build Revision
|
||||||
|
ubr, _, err := k.GetIntegerValue("UBR")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
ver := fmt.Sprintf("%d.%d.%s.%d", major, minor, build, ubr)
|
||||||
|
return ver
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
//go:build !(linux && 386)
|
||||||
|
// +build !linux !386
|
||||||
|
|
||||||
|
// skipping linux 32 bits build and tests
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
@@ -15,6 +17,8 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
@@ -58,6 +62,8 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
a := app.New()
|
a := app.New()
|
||||||
|
a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnectedPNG))
|
||||||
|
|
||||||
client := newServiceClient(daemonAddr, a, showSettings)
|
client := newServiceClient(daemonAddr, a, showSettings)
|
||||||
if showSettings {
|
if showSettings {
|
||||||
a.Run()
|
a.Run()
|
||||||
@@ -110,7 +116,7 @@ type serviceClient struct {
|
|||||||
iLogFile *widget.Entry
|
iLogFile *widget.Entry
|
||||||
iPreSharedKey *widget.Entry
|
iPreSharedKey *widget.Entry
|
||||||
|
|
||||||
// observable settings over correspondign iMngURL and iPreSharedKey values.
|
// observable settings over corresponding iMngURL and iPreSharedKey values.
|
||||||
managementURL string
|
managementURL string
|
||||||
preSharedKey string
|
preSharedKey string
|
||||||
adminURL string
|
adminURL string
|
||||||
@@ -118,7 +124,7 @@ type serviceClient struct {
|
|||||||
|
|
||||||
// newServiceClient instance constructor
|
// newServiceClient instance constructor
|
||||||
//
|
//
|
||||||
// This constructor olso build UI elements for settings window.
|
// This constructor also builds the UI elements for the settings window.
|
||||||
func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient {
|
func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient {
|
||||||
s := &serviceClient{
|
s := &serviceClient{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
@@ -146,7 +152,7 @@ func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient
|
|||||||
|
|
||||||
func (s *serviceClient) showUIElements() {
|
func (s *serviceClient) showUIElements() {
|
||||||
// add settings window UI elements.
|
// add settings window UI elements.
|
||||||
s.wSettings = s.app.NewWindow("Settings")
|
s.wSettings = s.app.NewWindow("NetBird Settings")
|
||||||
s.iMngURL = widget.NewEntry()
|
s.iMngURL = widget.NewEntry()
|
||||||
s.iAdminURL = widget.NewEntry()
|
s.iAdminURL = widget.NewEntry()
|
||||||
s.iConfigFile = widget.NewEntry()
|
s.iConfigFile = widget.NewEntry()
|
||||||
@@ -321,13 +327,15 @@ func (s *serviceClient) updateStatus() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.Status == string(internal.StatusConnected) {
|
if status.Status == string(internal.StatusConnected) && !s.mUp.Disabled() {
|
||||||
systray.SetIcon(s.icConnected)
|
systray.SetIcon(s.icConnected)
|
||||||
|
systray.SetTooltip("NetBird (Connected)")
|
||||||
s.mStatus.SetTitle("Connected")
|
s.mStatus.SetTitle("Connected")
|
||||||
s.mUp.Disable()
|
s.mUp.Disable()
|
||||||
s.mDown.Enable()
|
s.mDown.Enable()
|
||||||
} else {
|
} else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() {
|
||||||
systray.SetIcon(s.icDisconnected)
|
systray.SetIcon(s.icDisconnected)
|
||||||
|
systray.SetTooltip("NetBird (Disconnected)")
|
||||||
s.mStatus.SetTitle("Disconnected")
|
s.mStatus.SetTitle("Disconnected")
|
||||||
s.mDown.Disable()
|
s.mDown.Disable()
|
||||||
s.mUp.Enable()
|
s.mUp.Enable()
|
||||||
@@ -352,6 +360,7 @@ func (s *serviceClient) updateStatus() error {
|
|||||||
|
|
||||||
func (s *serviceClient) onTrayReady() {
|
func (s *serviceClient) onTrayReady() {
|
||||||
systray.SetIcon(s.icDisconnected)
|
systray.SetIcon(s.icDisconnected)
|
||||||
|
systray.SetTooltip("NetBird")
|
||||||
|
|
||||||
// setup systray menu items
|
// setup systray menu items
|
||||||
s.mStatus = systray.AddMenuItem("Disconnected", "Disconnected")
|
s.mStatus = systray.AddMenuItem("Disconnected", "Disconnected")
|
||||||
@@ -497,7 +506,7 @@ func (s *serviceClient) getSrvConfig() {
|
|||||||
// checkPIDFile exists and return error, or write new.
|
// checkPIDFile exists and return error, or write new.
|
||||||
func checkPIDFile() error {
|
func checkPIDFile() error {
|
||||||
pidFile := path.Join(os.TempDir(), "wiretrustee-ui.pid")
|
pidFile := path.Join(os.TempDir(), "wiretrustee-ui.pid")
|
||||||
if piddata, err := ioutil.ReadFile(pidFile); err == nil {
|
if piddata, err := os.ReadFile(pidFile); err == nil {
|
||||||
if pid, err := strconv.Atoi(string(piddata)); err == nil {
|
if pid, err := strconv.Atoi(string(piddata)); err == nil {
|
||||||
if process, err := os.FindProcess(pid); err == nil {
|
if process, err := os.FindProcess(pid); err == nil {
|
||||||
if err := process.Signal(syscall.Signal(0)); err == nil {
|
if err := process.Signal(syscall.Signal(0)); err == nil {
|
||||||
@@ -507,5 +516,5 @@ func checkPIDFile() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ioutil.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0o664)
|
return os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0o664)
|
||||||
}
|
}
|
||||||
|
|||||||
109
dns/dns.go
Normal file
109
dns/dns.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// Package dns implement dns types and standard methods and functions
|
||||||
|
// to parse and normalize dns records and configuration
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"golang.org/x/net/idna"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultDNSPort well-known port number
|
||||||
|
DefaultDNSPort = 53
|
||||||
|
// RootZone is a string representation of the root zone
|
||||||
|
RootZone = "."
|
||||||
|
// DefaultClass is the class supported by the system
|
||||||
|
DefaultClass = "IN"
|
||||||
|
)
|
||||||
|
|
||||||
|
const invalidHostLabel = "[^a-zA-Z0-9-]+"
|
||||||
|
|
||||||
|
// Config represents a dns configuration that is exchanged between management and peers
|
||||||
|
type Config struct {
|
||||||
|
// ServiceEnable indicates if the service should be enabled
|
||||||
|
ServiceEnable bool
|
||||||
|
// NameServerGroups contains a list of nameserver group
|
||||||
|
NameServerGroups []*NameServerGroup
|
||||||
|
// CustomZones contains a list of custom zone
|
||||||
|
CustomZones []CustomZone
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomZone represents a custom zone to be resolved by the dns server
|
||||||
|
type CustomZone struct {
|
||||||
|
// Domain is the zone's domain
|
||||||
|
Domain string
|
||||||
|
// Records custom zone records
|
||||||
|
Records []SimpleRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimpleRecord provides a simple DNS record specification for CNAME, A and AAAA records
|
||||||
|
type SimpleRecord struct {
|
||||||
|
// Name domain name
|
||||||
|
Name string
|
||||||
|
// Type of record, 1 for A, 5 for CNAME, 28 for AAAA. see https://pkg.go.dev/github.com/miekg/dns@v1.1.41#pkg-constants
|
||||||
|
Type int
|
||||||
|
// Class dns class, currently use the DefaultClass for all records
|
||||||
|
Class string
|
||||||
|
// TTL time-to-live for the record
|
||||||
|
TTL int
|
||||||
|
// RData is the actual value resolved in a dns query
|
||||||
|
RData string
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string of the simple record formatted as:
|
||||||
|
// <Name> <TTL> <Class> <Type> <RDATA>
|
||||||
|
func (s SimpleRecord) String() string {
|
||||||
|
fqdn := dns.Fqdn(s.Name)
|
||||||
|
return fmt.Sprintf("%s %d %s %s %s", fqdn, s.TTL, s.Class, dns.Type(s.Type).String(), s.RData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the length of the RData field, based on its type
|
||||||
|
func (s SimpleRecord) Len() uint16 {
|
||||||
|
emptyString := s.RData == ""
|
||||||
|
switch s.Type {
|
||||||
|
case 1:
|
||||||
|
if emptyString {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return net.IPv4len
|
||||||
|
case 5:
|
||||||
|
if emptyString || s.RData == "." {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return uint16(len(s.RData) + 1)
|
||||||
|
case 28:
|
||||||
|
if emptyString {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return net.IPv6len
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParsedDomainLabel returns a domain label with max 59 characters,
|
||||||
|
// parsed for old Hosts.txt requirements, and converted to ASCII and lowercase
|
||||||
|
func GetParsedDomainLabel(name string) (string, error) {
|
||||||
|
labels := dns.SplitDomainName(name)
|
||||||
|
if len(labels) == 0 {
|
||||||
|
return "", fmt.Errorf("got empty label list for name \"%s\"", name)
|
||||||
|
}
|
||||||
|
rawLabel := labels[0]
|
||||||
|
ascii, err := idna.Punycode.ToASCII(rawLabel)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to convert host lavel to ASCII, error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidHostMatcher := regexp.MustCompile(invalidHostLabel)
|
||||||
|
|
||||||
|
validHost := strings.ToLower(invalidHostMatcher.ReplaceAllString(ascii, "-"))
|
||||||
|
if len(validHost) > 58 {
|
||||||
|
validHost = validHost[:59]
|
||||||
|
}
|
||||||
|
|
||||||
|
return validHost, nil
|
||||||
|
}
|
||||||
192
dns/nameserver.go
Normal file
192
dns/nameserver.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// InvalidNameServerType invalid nameserver type
|
||||||
|
InvalidNameServerType NameServerType = iota
|
||||||
|
// UDPNameServerType udp nameserver type
|
||||||
|
UDPNameServerType
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MaxGroupNameChar maximum group name size
|
||||||
|
MaxGroupNameChar = 40
|
||||||
|
// InvalidNameServerTypeString invalid nameserver type as string
|
||||||
|
InvalidNameServerTypeString = "invalid"
|
||||||
|
// UDPNameServerTypeString udp nameserver type as string
|
||||||
|
UDPNameServerTypeString = "udp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NameServerType nameserver type
|
||||||
|
type NameServerType int
|
||||||
|
|
||||||
|
// String returns nameserver type string
|
||||||
|
func (n NameServerType) String() string {
|
||||||
|
switch n {
|
||||||
|
case UDPNameServerType:
|
||||||
|
return UDPNameServerTypeString
|
||||||
|
default:
|
||||||
|
return InvalidNameServerTypeString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToNameServerType returns a nameserver type
|
||||||
|
func ToNameServerType(typeString string) NameServerType {
|
||||||
|
switch typeString {
|
||||||
|
case UDPNameServerTypeString:
|
||||||
|
return UDPNameServerType
|
||||||
|
default:
|
||||||
|
return InvalidNameServerType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameServerGroup group of nameservers and with group ids
|
||||||
|
type NameServerGroup struct {
|
||||||
|
// ID identifier of group
|
||||||
|
ID string
|
||||||
|
// Name group name
|
||||||
|
Name string
|
||||||
|
// Description group description
|
||||||
|
Description string
|
||||||
|
// NameServers list of nameservers
|
||||||
|
NameServers []NameServer
|
||||||
|
// Groups list of peer group IDs to distribute the nameservers information
|
||||||
|
Groups []string
|
||||||
|
// Primary indicates that the nameserver group is the primary resolver for any dns query
|
||||||
|
Primary bool
|
||||||
|
// Domains indicate the dns query domains to use with this nameserver group
|
||||||
|
Domains []string
|
||||||
|
// Enabled group status
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameServer represents a DNS nameserver
|
||||||
|
type NameServer struct {
|
||||||
|
// IP address of nameserver
|
||||||
|
IP netip.Addr
|
||||||
|
// NSType nameserver type
|
||||||
|
NSType NameServerType
|
||||||
|
// Port nameserver listening port
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies a nameserver object
|
||||||
|
func (n *NameServer) Copy() *NameServer {
|
||||||
|
return &NameServer{
|
||||||
|
IP: n.IP,
|
||||||
|
NSType: n.NSType,
|
||||||
|
Port: n.Port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEqual compares one nameserver with the other
|
||||||
|
func (n *NameServer) IsEqual(other *NameServer) bool {
|
||||||
|
return other.IP == n.IP &&
|
||||||
|
other.NSType == n.NSType &&
|
||||||
|
other.Port == n.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseNameServerURL parses a nameserver url in the format <type>://<ip>:<port>, e.g., udp://1.1.1.1:53
|
||||||
|
func ParseNameServerURL(nsURL string) (NameServer, error) {
|
||||||
|
parsedURL, err := url.Parse(nsURL)
|
||||||
|
if err != nil {
|
||||||
|
return NameServer{}, err
|
||||||
|
}
|
||||||
|
var ns NameServer
|
||||||
|
parsedScheme := strings.ToLower(parsedURL.Scheme)
|
||||||
|
nsType := ToNameServerType(parsedScheme)
|
||||||
|
if nsType == InvalidNameServerType {
|
||||||
|
return NameServer{}, fmt.Errorf("invalid nameserver url schema type, got %s", parsedScheme)
|
||||||
|
}
|
||||||
|
ns.NSType = nsType
|
||||||
|
|
||||||
|
parsedPort, err := strconv.Atoi(parsedURL.Port())
|
||||||
|
if err != nil {
|
||||||
|
return NameServer{}, fmt.Errorf("invalid nameserver url port, got %s", parsedURL.Port())
|
||||||
|
}
|
||||||
|
ns.Port = parsedPort
|
||||||
|
|
||||||
|
parsedAddr, err := netip.ParseAddr(parsedURL.Hostname())
|
||||||
|
if err != nil {
|
||||||
|
return NameServer{}, fmt.Errorf("invalid nameserver url IP, got %s", parsedURL.Hostname())
|
||||||
|
}
|
||||||
|
|
||||||
|
ns.IP = parsedAddr
|
||||||
|
|
||||||
|
return ns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies a nameserver group object
|
||||||
|
func (g *NameServerGroup) Copy() *NameServerGroup {
|
||||||
|
return &NameServerGroup{
|
||||||
|
ID: g.ID,
|
||||||
|
Name: g.Name,
|
||||||
|
Description: g.Description,
|
||||||
|
NameServers: g.NameServers,
|
||||||
|
Groups: g.Groups,
|
||||||
|
Enabled: g.Enabled,
|
||||||
|
Primary: g.Primary,
|
||||||
|
Domains: g.Domains,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEqual compares one nameserver group with the other
|
||||||
|
func (g *NameServerGroup) IsEqual(other *NameServerGroup) bool {
|
||||||
|
return other.ID == g.ID &&
|
||||||
|
other.Name == g.Name &&
|
||||||
|
other.Description == g.Description &&
|
||||||
|
other.Primary == g.Primary &&
|
||||||
|
compareNameServerList(g.NameServers, other.NameServers) &&
|
||||||
|
compareGroupsList(g.Groups, other.Groups) &&
|
||||||
|
compareGroupsList(g.Domains, other.Domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareNameServerList(list, other []NameServer) bool {
|
||||||
|
if len(list) != len(other) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ns := range list {
|
||||||
|
if !containsNameServer(ns, other) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsNameServer(element NameServer, list []NameServer) bool {
|
||||||
|
for _, ns := range list {
|
||||||
|
if ns.IsEqual(&element) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareGroupsList(list, other []string) bool {
|
||||||
|
if len(list) != len(other) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, id := range list {
|
||||||
|
match := false
|
||||||
|
for _, otherID := range other {
|
||||||
|
if id == otherID {
|
||||||
|
match = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
104
docs/README.md
104
docs/README.md
@@ -1,104 +0,0 @@
|
|||||||
### Table of contents
|
|
||||||
|
|
||||||
* [About Netbird](#about-netbird)
|
|
||||||
* [Why Wireguard with Netbird?](#why-wireguard-with-netbird)
|
|
||||||
* [Netbird vs. Traditional VPN](#netbird-vs-traditional-vpn)
|
|
||||||
* [High-level technology overview](#high-level-technology-overview)
|
|
||||||
* [Getting started](#getting-started)
|
|
||||||
|
|
||||||
### About Netbird
|
|
||||||
|
|
||||||
Netbird is an open-source VPN platform built on top of [WireGuard®](https://www.wireguard.com/) making it easy to create secure private networks for your organization or home.
|
|
||||||
|
|
||||||
It requires zero configuration effort leaving behind the hassle of opening ports, complex firewall rules, vpn gateways, and so forth.
|
|
||||||
|
|
||||||
There is no centralized VPN server with Netbird - your computers, devices, machines, and servers connect to each other directly over a fast encrypted tunnel.
|
|
||||||
|
|
||||||
It literally takes less than 5 minutes to provision a secure peer-to-peer VPN with Netbird. Check our [Quickstart Guide Video](https://www.youtube.com/watch?v=cWTsGUJAUaU) to see the setup in action.
|
|
||||||
|
|
||||||
### Why Wireguard with Netbird?
|
|
||||||
|
|
||||||
WireGuard is a modern and extremely fast VPN tunnel utilizing state-of-the-art [cryptography](https://www.wireguard.com/protocol/)
|
|
||||||
and Netbird uses Wireguard to establish a secure tunnel between machines.
|
|
||||||
|
|
||||||
Built with simplicity in mind, Wireguard ensures that traffic between two machines is encrypted and flowing, however, it requires a few things to be done beforehand.
|
|
||||||
|
|
||||||
First, in order to connect, the machines have to be configured.
|
|
||||||
On each machine, you need to generate private and public keys and prepare a WireGuard configuration file.
|
|
||||||
The configuration also includes a private IP address that should be unique per machine.
|
|
||||||
|
|
||||||
Secondly, to accept the incoming traffic, the machines have to trust each other.
|
|
||||||
The generated public keys have to be pre-shared on the machines.
|
|
||||||
This works similarly to SSH with its authorised_keys file.
|
|
||||||
|
|
||||||
Lastly, the connectivity between the machines has to be ensured.
|
|
||||||
To make machines reach one another, you are required to set a WireGuard endpoint property which indicates the IP address and port of the remote machine to connect to.
|
|
||||||
On many occasions, machines are hidden behind firewalls and NAT devices,
|
|
||||||
meaning that you may need to configure a port forwarding or open holes in your firewall to ensure the machines are reachable.
|
|
||||||
|
|
||||||
The undertakings mentioned above might not be complicated if you have just a few machines, but the complexity grows as the number of machines increases.
|
|
||||||
|
|
||||||
Netbird simplifies the setup by automatically generating private and public keys, assigning unique private IP addresses, and takes care of sharing public keys between the machines.
|
|
||||||
It is worth mentioning that the private key never leaves the machine.
|
|
||||||
So only the machine that owns the key can decrypt traffic addressed to it.
|
|
||||||
The same applies also to the relayed traffic mentioned below.
|
|
||||||
|
|
||||||
Furthermore, Netbird ensures connectivity by leveraging advanced [NAT traversal techniques](https://en.wikipedia.org/wiki/NAT_traversal)
|
|
||||||
and removing the necessity of port forwarding, opening holes in the firewall, and having a public static IP address.
|
|
||||||
In cases when a direct peer-to-peer connection isn't possible, all traffic is relayed securely between peers.
|
|
||||||
Netbird also monitors the connection health and restarts broken connections.
|
|
||||||
|
|
||||||
There are a few more things that we are working on to make secure private networks simple. A few examples are ACLs, MFA and activity monitoring.
|
|
||||||
|
|
||||||
Check out the WireGuard [Quick Start](https://www.wireguard.com/quickstart/) guide to learn more about configuring "plain" WireGuard without Netbird.
|
|
||||||
|
|
||||||
### Netbird vs. Traditional VPN
|
|
||||||
|
|
||||||
In the traditional VPN model, everything converges on a centralized, protected network where all the clients are connecting to a central VPN server.
|
|
||||||
|
|
||||||
An increasing amount of connections can easily overload the VPN server.
|
|
||||||
Even a short downtime of a server can cause expensive system disruptions, and a remote team's inability to work.
|
|
||||||
|
|
||||||
Centralized VPNs imply all the traffic going through the central server causing network delays and increased traffic usage.
|
|
||||||
|
|
||||||
Such systems require an experienced team to set up and maintain.
|
|
||||||
Configuring firewalls, setting up NATs, SSO integration, and managing access control lists can be a nightmare.
|
|
||||||
|
|
||||||
Traditional centralized VPNs are often compared to a [castle-and-moat](https://en.wikipedia.org/wiki/Moat) model
|
|
||||||
in which once accessed, user is trusted and can access critical infrastructure and resources without any restrictions.
|
|
||||||
|
|
||||||
Netbird decentralizes networks using direct point-to-point connections, as opposed to traditional models.
|
|
||||||
Consequently, network performance is increased since traffic flows directly between the machines bypassing VPN servers or gateways.
|
|
||||||
To achieve this, Netbird client applications employ signalling servers to find other machines and negotiate connections.
|
|
||||||
These are similar to the signaling servers used in [WebRTC](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling#the_signaling_server)
|
|
||||||
|
|
||||||
Thanks to [NAT traversal techniques](https://en.wikipedia.org/wiki/NAT_traversal),
|
|
||||||
outlined in the [Why not just Wireguard?](#why-wireguard-with-netbird) section above,
|
|
||||||
Netbird installation doesn't require complex network and firewall configuration.
|
|
||||||
It just works, minimising the maintenance effort.
|
|
||||||
|
|
||||||
Finally, each machine or device in the Netbird network verifies incoming connections accepting only the trusted ones.
|
|
||||||
This is ensured by Wireguard's [Crypto Routing concept](https://www.wireguard.com/#cryptokey-routing).
|
|
||||||
|
|
||||||
### High-level technology overview
|
|
||||||
In essence, Netbird is an open source platform consisting of a collection of systems, responsible for handling peer-to-peer connections, tunneling and network management (IP, keys, ACLs, etc).
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="media/high-level-dia.png" alt="high-level-dia" width="781"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
Netbird uses open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), [Coturn](https://github.com/coturn/coturn),
|
|
||||||
and [software](https://github.com/netbirdio/netbird) developed by Netbird authors to make it all work together.
|
|
||||||
|
|
||||||
To learn more about Netbird architecture, please refer to the [architecture section](../docs/architecture.md).
|
|
||||||
|
|
||||||
### Getting Started
|
|
||||||
|
|
||||||
There are 2 ways of getting started with Netbird:
|
|
||||||
- use Cloud Managed version
|
|
||||||
- self-hosting
|
|
||||||
|
|
||||||
We recommend starting with the cloud managed version hosted at [app.netbird.io](https://app.netbird.io) - the quickest way to get familiar with the system.
|
|
||||||
See [Quickstart Guide](../docs/quickstart.md) for instructions.
|
|
||||||
|
|
||||||
If you don't want to use the managed version, check out our [Self-hosting Guide](../docs/self-hosting.md).
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
### Architecture
|
|
||||||
TODO
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.9 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
@@ -1,41 +0,0 @@
|
|||||||
## Quickstart guide (Cloud Managed version)
|
|
||||||
Step-by-step video guide on YouTube:
|
|
||||||
|
|
||||||
[](https://youtu.be/cWTsGUJAUaU "Netbird - secure private network in less than 5 minutes")
|
|
||||||
|
|
||||||
This guide describes how to create secure VPN and connect 2 machines peer-to-peer.
|
|
||||||
|
|
||||||
One machine is a Raspberry Pi Compute Module 4 hosted at home (Peer A), and the other one is a regular Ubuntu server running in the Data Center (Peer B).
|
|
||||||
Both machines are running Linux (Raspbian and Ubuntu respectively), but you could also use Mac or Windows operating systems.
|
|
||||||
|
|
||||||
1. Sign-up at [https://app.netbird.io/](https://app.netbird.io/)
|
|
||||||
|
|
||||||
You can use your email and password to sign-up or any available social login option (e.g., GitHub account)
|
|
||||||
|
|
||||||
<img src="media/auth.png" alt="auth" width="350"/>
|
|
||||||
|
|
||||||
2. After a successful login you will be redirected to the ```Peers``` screen which is empty because you don't have any peers yet.
|
|
||||||
|
|
||||||
Click ```Add peer``` to add a new machine.
|
|
||||||
|
|
||||||
<img src="media/empty-peers.png" alt="empty-peers" width="700"/>
|
|
||||||
|
|
||||||
3. Choose a setup key which will be used to associate your new machine with your account (in our case it is ```Default key```).
|
|
||||||
|
|
||||||
Choose your machine operating system (in our case it is ```Linux```) and proceed with the installation steps on the machine.
|
|
||||||
|
|
||||||
<img src="media/add-peer.png" alt="add-peer" width="700"/>
|
|
||||||
|
|
||||||
4. Repeat #3 for the 2nd machine.
|
|
||||||
5. Return to ```Peers``` and you should notice 2 new machines with status ```Connected```
|
|
||||||
|
|
||||||
<img src="media/peers.png" alt="peers" width="700"/>
|
|
||||||
|
|
||||||
6. To test the connection you could try pinging devices:
|
|
||||||
|
|
||||||
On Peer A:
|
|
||||||
```ping 100.64.0.2```
|
|
||||||
|
|
||||||
On Peer B:
|
|
||||||
```ping 100.64.0.1```
|
|
||||||
7. Done! You now have a secure peer-to-peer VPN configured.
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
### Self-hosting
|
|
||||||
Netbird is an open-source platform that can be self-hosted on your servers.
|
|
||||||
|
|
||||||
It relies on components developed by Netbird Authors [Management Service](https://github.com/netbirdio/netbird/tree/main/management), [Management UI Dashboard](https://github.com/netbirdio/dashboard), [Signal Service](https://github.com/netbirdio/netbird/tree/main/signal),
|
|
||||||
a 3rd party open-source STUN/TURN service [Coturn](https://github.com/coturn/coturn) and a 3rd party service [Auth0](https://auth0.com/).
|
|
||||||
|
|
||||||
All the components can be self-hosted except for the Auth0 service.
|
|
||||||
We chose Auth0 to "outsource" the user management part of the platform because we believe that implementing a proper user auth requires significant amount of time to make it right.
|
|
||||||
We focused on connectivity instead. It also offers an always free plan that should be ok for most users as its limits are high enough for most teams.
|
|
||||||
|
|
||||||
If you would like to learn more about the architecture please refer to the [Netbird Architecture section](architecture.md).
|
|
||||||
|
|
||||||
### Step-by-step video guide on YouTube:
|
|
||||||
|
|
||||||
[](https://youtu.be/Ofpgx5WhT0k "Netbird Self-Hosting Guide")
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
- Virtual machine offered by any cloud provider (e.g., AWS, DigitalOcean, Hetzner, Google Cloud, Azure ...).
|
|
||||||
- Any Unix OS.
|
|
||||||
- Docker Compose installed (see [Install Docker Compose](https://docs.docker.com/compose/install/)).
|
|
||||||
- Domain name pointing to the public IP address of your server.
|
|
||||||
- Netbird Open ports ```443, 33071, 33073, 10000``` (Dashboard, Management HTTP API, Management gRpc API, Signal gRpc) on your server.
|
|
||||||
- Coturn is used for relay using the STUN/TURN protocols. It requires a listening port, ```UDP 3478```, and range of ports,```UDP 49152-65535```, for dynamic relay connections. These are set as defaults in [setup file](https://github.com/netbirdio/netbird/blob/main/infrastructure_files/setup.env#L34), but can be configured to your requirements.
|
|
||||||
- Maybe a cup of coffee or tea :)
|
|
||||||
|
|
||||||
### Step-by-step guide
|
|
||||||
|
|
||||||
For this tutorial we will be using domain ```test.netbird.io``` which points to our Ubuntu 20.04 machine hosted at Hetzner.
|
|
||||||
|
|
||||||
1. Create Auth0 account at [auth0.com](https://auth0.com/).
|
|
||||||
2. Login to your server, clone Netbird repository:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/netbirdio/netbird.git netbird/
|
|
||||||
```
|
|
||||||
|
|
||||||
and switch to the ```netbird/infrastructure_files/``` folder that contains docker compose file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd netbird/infrastructure_files/
|
|
||||||
```
|
|
||||||
3. Prepare configuration files.
|
|
||||||
|
|
||||||
To simplify the setup we have prepared a script to substitute required properties in the [turnserver.conf.tmpl](../infrastructure_files/turnserver.conf.tmpl),[docker-compose.yml.tmpl](../infrastructure_files/docker-compose.yml.tmpl) and [management.json.tmpl](../infrastructure_files/management.json.tmpl) files.
|
|
||||||
|
|
||||||
The [setup.env](../infrastructure_files/setup.env) file contains the following properties that have to be filled:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Dashboard domain. e.g. app.mydomain.com
|
|
||||||
NETBIRD_DOMAIN=""
|
|
||||||
# e.g. dev-24vkclam.us.auth0.com
|
|
||||||
NETBIRD_AUTH0_DOMAIN=""
|
|
||||||
# e.g. 61u3JMXRO0oOevc7gCkZLCwePQvT4lL0
|
|
||||||
NETBIRD_AUTH0_CLIENT_ID=""
|
|
||||||
# e.g. https://app.mydomain.com/ or https://app.mydomain.com,
|
|
||||||
# Make sure you used the exact same value for Identifier
|
|
||||||
# you used when creating your Auth0 API
|
|
||||||
NETBIRD_AUTH0_AUDIENCE=""
|
|
||||||
# e.g. hello@mydomain.com
|
|
||||||
NETBIRD_LETSENCRYPT_EMAIL=""
|
|
||||||
```
|
|
||||||
> Other options are available, but they are automatically updated.
|
|
||||||
|
|
||||||
Please follow the steps to get the values.
|
|
||||||
|
|
||||||
4. Configure ```NETBIRD_AUTH0_DOMAIN``` ```NETBIRD_AUTH0_CLIENT_ID``` ```NETBIRD_AUTH0_AUDIENCE``` properties.
|
|
||||||
|
|
||||||
* To obtain these, please use [Auth0 React SDK Guide](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0) up until "Install the Auth0 React SDK".
|
|
||||||
|
|
||||||
:grey_exclamation: Use ```https://YOUR DOMAIN``` as ````Allowed Callback URLs````, ```Allowed Logout URLs```, ```Allowed Web Origins``` and ```Allowed Origins (CORS)```
|
|
||||||
* set the variables in the ```setup.env```
|
|
||||||
5. Configure ```NETBIRD_AUTH0_AUDIENCE``` property.
|
|
||||||
|
|
||||||
* Check [Auth0 Golang API Guide](https://auth0.com/docs/quickstart/backend/golang) to obtain AuthAudience.
|
|
||||||
* set the property in the ```setup.env``` file.
|
|
||||||
6. Configure ```NETBIRD_LETSENCRYPT_EMAIL``` property.
|
|
||||||
|
|
||||||
This can be any email address. [Let's Encrypt](https://letsencrypt.org/) will create an account while generating a new certificate.
|
|
||||||
|
|
||||||
7. Make sure all the properties set in the ```setup.env``` file and run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./configure.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This will export all the properties as environment variables and generate ```docker-compose.yml``` and ```management.json``` files substituting required variables.
|
|
||||||
|
|
||||||
8. Run docker compose:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
9. Optionally check the logs by running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose logs signal
|
|
||||||
docker-compose logs management
|
|
||||||
docker-compose logs coturn
|
|
||||||
docker-compose logs dashboard
|
|
||||||
|
|
||||||
10. Once the server is running, you can access the dashboard by https://$NETBIRD_DOMAIN
|
|
||||||
11. Adding a peer will require you to enter the management URL by following the steps in the page https://$NETBIRD_DOMAIN/add-peer and in the 3rd step:
|
|
||||||
```shell
|
|
||||||
sudo netbird up --setup-key <PASTE-SETUP-KEY> --management-url https://$NETBIRD_DOMAIN:33073
|
|
||||||
```
|
|
||||||
@@ -8,17 +8,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// CreateCertManager wraps common logic of generating Let's encrypt certificate.
|
// CreateCertManager wraps common logic of generating Let's encrypt certificate.
|
||||||
func CreateCertManager(datadir string, letsencryptDomain string) *autocert.Manager {
|
func CreateCertManager(datadir string, letsencryptDomain string) (*autocert.Manager, error) {
|
||||||
certDir := filepath.Join(datadir, "letsencrypt")
|
certDir := filepath.Join(datadir, "letsencrypt")
|
||||||
|
|
||||||
if _, err := os.Stat(certDir); os.IsNotExist(err) {
|
if _, err := os.Stat(certDir); os.IsNotExist(err) {
|
||||||
err = os.MkdirAll(certDir, os.ModeDir)
|
err = os.MkdirAll(certDir, os.ModeDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed creating Let's encrypt certdir: %s: %v", certDir, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("running with Let's encrypt with domain %s. Cert will be stored in %s", letsencryptDomain, certDir)
|
log.Infof("running with LetsEncrypt (%s). Cert will be stored in %s", letsencryptDomain, certDir)
|
||||||
|
|
||||||
certManager := &autocert.Manager{
|
certManager := &autocert.Manager{
|
||||||
Prompt: autocert.AcceptTOS,
|
Prompt: autocert.AcceptTOS,
|
||||||
@@ -26,5 +26,5 @@ func CreateCertManager(datadir string, letsencryptDomain string) *autocert.Manag
|
|||||||
HostPolicy: autocert.HostWhitelist(letsencryptDomain),
|
HostPolicy: autocert.HostWhitelist(letsencryptDomain),
|
||||||
}
|
}
|
||||||
|
|
||||||
return certManager
|
return certManager, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user