mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-03 16:13:53 -04:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
8
.github/workflows/golang-test-darwin.yml
vendored
8
.github/workflows/golang-test-darwin.yml
vendored
@@ -1,16 +1,14 @@
|
||||
name: Test Code Darwin
|
||||
on: [push,pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.18.x]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
go-version: 1.18.x
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -26,4 +24,4 @@ jobs:
|
||||
run: go mod tidy
|
||||
|
||||
- name: Test
|
||||
run: go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
|
||||
run: go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
|
||||
|
||||
7
.github/workflows/golang-test-linux.yml
vendored
7
.github/workflows/golang-test-linux.yml
vendored
@@ -1,16 +1,17 @@
|
||||
name: Test Code Linux
|
||||
on: [push,pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.18.x]
|
||||
arch: ['386','amd64']
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
go-version: 1.18.x
|
||||
|
||||
|
||||
- name: Cache Go modules
|
||||
@@ -31,4 +32,4 @@ jobs:
|
||||
run: go mod tidy
|
||||
|
||||
- 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 ./...
|
||||
|
||||
19
.github/workflows/golang-test-windows.yml
vendored
19
.github/workflows/golang-test-windows.yml
vendored
@@ -1,5 +1,6 @@
|
||||
name: Test Code Windows
|
||||
on: [push,pull_request]
|
||||
|
||||
jobs:
|
||||
pre:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -17,13 +18,8 @@ jobs:
|
||||
|
||||
test:
|
||||
needs: pre
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.18.x]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: disable defender
|
||||
run: Set-MpPreference -DisableRealtimeMonitoring $true
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
@@ -31,27 +27,22 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
go-version: 1.18.x
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
%LocalAppData%\go-build
|
||||
~/go/pkg/mod
|
||||
~\go\pkg\mod
|
||||
~\AppData\Local\go-build
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: enable defender
|
||||
run: Set-MpPreference -DisableRealtimeMonitoring $false
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: syso
|
||||
path: iface\
|
||||
|
||||
# - name: Install modules
|
||||
# run: go mod tidy
|
||||
|
||||
- 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
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
- 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: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
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
@@ -10,6 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
SIGN_PIPE_VER: "v0.0.3"
|
||||
GORELEASER_VER: "v1.6.3"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -40,6 +41,9 @@ jobs:
|
||||
-
|
||||
name: Install modules
|
||||
run: go mod tidy
|
||||
-
|
||||
name: check git status
|
||||
run: git --no-pager diff --exit-code
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
@@ -54,45 +58,75 @@ jobs:
|
||||
username: netbirdio
|
||||
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
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: v1.6.3
|
||||
version: ${{ env.GORELEASER_VER }}
|
||||
args: release --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: 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
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
name: release
|
||||
path: dist/
|
||||
retention-days: 3
|
||||
|
||||
|
||||
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
|
||||
steps:
|
||||
-
|
||||
@@ -110,9 +144,9 @@ jobs:
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
key: ${{ runner.os }}-ui-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
${{ runner.os }}-ui-go-
|
||||
-
|
||||
name: Install modules
|
||||
run: go mod tidy
|
||||
@@ -121,26 +155,42 @@ jobs:
|
||||
id: goreleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: v1.6.3
|
||||
version: ${{ env.GORELEASER_VER }}
|
||||
args: release --config .goreleaser_ui_darwin.yaml --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
-
|
||||
name: Trigger Darwin App binaries sign pipeline
|
||||
uses: benc-uk/workflow-dispatch@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
name: upload non tags for debug purposes
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
workflow: Sign darwin ui app with dispatch
|
||||
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/')
|
||||
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 }}" }'
|
||||
|
||||
-
|
||||
name: upload non tags for debug purposes
|
||||
uses: actions/upload-artifact@v2
|
||||
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:
|
||||
name: build-ui-darwin
|
||||
path: dist/
|
||||
retention-days: 3
|
||||
workflow: Sign darwin ui app with dispatch
|
||||
repo: netbirdio/sign-pipelines
|
||||
ref: ${{ env.SIGN_PIPE_VER }}
|
||||
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
||||
inputs: '{ "tag": "${{ github.ref }}" }'
|
||||
72
.github/workflows/test-docker-compose-linux.yml
vendored
Normal file
72
.github/workflows/test-docker-compose-linux.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: Test Docker Compose Linux
|
||||
on: [push,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
|
||||
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"
|
||||
|
||||
- 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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
.idea
|
||||
.run
|
||||
*.iml
|
||||
dist/
|
||||
bin/
|
||||
@@ -9,3 +10,4 @@ infrastructure_files/management.json
|
||||
infrastructure_files/docker-compose.yml
|
||||
*.syso
|
||||
client/.distfiles/
|
||||
infrastructure_files/setup.env
|
||||
@@ -13,6 +13,7 @@ builds:
|
||||
- amd64
|
||||
- arm64
|
||||
- mips
|
||||
- 386
|
||||
gomips:
|
||||
- hardfloat
|
||||
- softfloat
|
||||
@@ -21,6 +22,8 @@ builds:
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: 386
|
||||
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 }}'
|
||||
@@ -55,88 +58,12 @@ builds:
|
||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||
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:
|
||||
- builds:
|
||||
- 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:
|
||||
|
||||
- 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>
|
||||
description: Netbird client.
|
||||
homepage: https://netbird.io/
|
||||
@@ -428,7 +355,6 @@ uploads:
|
||||
- name: debian
|
||||
ids:
|
||||
- netbird-deb
|
||||
- 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
|
||||
@@ -437,7 +363,6 @@ uploads:
|
||||
- name: yum
|
||||
ids:
|
||||
- netbird-rpm
|
||||
- netbird-ui-rpm
|
||||
mode: archive
|
||||
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
||||
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
|
||||
- softfloat
|
||||
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 }}'
|
||||
tags:
|
||||
- load_wgnt_from_rsrc
|
||||
@@ -23,5 +23,7 @@ archives:
|
||||
- builds:
|
||||
- netbird-ui-darwin
|
||||
|
||||
checksum:
|
||||
name_template: "{{ .ProjectName }}_darwin_checksums.txt"
|
||||
changelog:
|
||||
skip: true
|
||||
82
README.md
82
README.md
@@ -1,31 +1,21 @@
|
||||
<p align="center">
|
||||
<strong>:hatching_chick: New release! Beta Update May 2022</strong>.
|
||||
<a href="https://github.com/netbirdio/netbird/releases/tag/v0.6.0">
|
||||
<strong>:hatching_chick: New release! NetBird Easy SSH</strong>.
|
||||
<a href="https://github.com/netbirdio/netbird/releases/tag/v0.8.0">
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
<div align="center">
|
||||
|
||||
<p align="center">
|
||||
<img width="234" src="docs/media/logo-full.png"/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/netbirdio/netbird/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/wiretrustee/wiretrustee/tags">
|
||||
<img src="https://img.shields.io/docker/pulls/wiretrustee/wiretrustee" />
|
||||
</a>
|
||||
<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>
|
||||
<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://goreportcard.com/report/wiretrustee/wiretrustee">
|
||||
<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">
|
||||
<a href="https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">
|
||||
<img src="https://img.shields.io/badge/slack-@wiretrustee-red.svg?logo=slack"/>
|
||||
</a>
|
||||
</p>
|
||||
@@ -38,7 +28,7 @@
|
||||
<br/>
|
||||
See <a href="https://netbird.io/docs/">Documentation</a>
|
||||
<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/>
|
||||
|
||||
</strong>
|
||||
@@ -53,20 +43,23 @@ It requires zero configuration effort leaving behind the hassle of opening ports
|
||||
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.
|
||||
|
||||
**Key features:**
|
||||
* Automatic IP allocation and management.
|
||||
* Automatic WireGuard peer (machine) discovery and configuration.
|
||||
* Encrypted peer-to-peer connections without a central VPN gateway.
|
||||
* 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))
|
||||
* Desktop client applications for Linux, MacOS, and Windows.
|
||||
* Multiuser support - sharing network between multiple users.
|
||||
* SSO and MFA support.
|
||||
* Multicloud and hybrid-cloud support.
|
||||
* Kernel WireGuard usage when possible.
|
||||
* Access Controls - groups & rules (coming soon).
|
||||
* Private DNS (coming soon).
|
||||
* Mobile clients (coming soon).
|
||||
* Network Activity Monitoring (coming soon).
|
||||
- \[x] Automatic IP allocation and network management with a Web UI ([separate repo](https://github.com/netbirdio/dashboard))
|
||||
- \[x] Automatic WireGuard peer (machine) discovery and configuration.
|
||||
- \[x] Encrypted peer-to-peer connections without a central VPN gateway.
|
||||
- \[x] Connection relay fallback in case a peer-to-peer connection is not possible.
|
||||
- \[x] Desktop client applications for Linux, MacOS, and Windows (systray).
|
||||
- \[x] Multiuser support - sharing network between multiple users.
|
||||
- \[x] SSO and MFA support.
|
||||
- \[x] Multicloud and hybrid-cloud support.
|
||||
- \[x] Kernel WireGuard usage when possible.
|
||||
- \[x] Access Controls - groups & rules.
|
||||
- \[x] Remote SSH access without managing SSH keys.
|
||||
|
||||
**Coming soon:**
|
||||
- \[ ] Router nodes
|
||||
- \[ ] Private DNS.
|
||||
- \[ ] Mobile clients.
|
||||
- \[ ] Network Activity Monitoring.
|
||||
|
||||
### Secure peer-to-peer VPN with SSO and MFA in minutes
|
||||
<p float="left" align="middle">
|
||||
@@ -78,23 +71,21 @@ NetBird creates an overlay peer-to-peer network connecting machines automaticall
|
||||
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
|
||||
|
||||
### Start using NetBird
|
||||
* Hosted version: [https://app.netbird.io/](https://app.netbird.io/).
|
||||
* 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).
|
||||
* Step-by-step [Installation Guide](https://netbird.io/docs/getting-started/installation) for different platforms.
|
||||
* Web UI [repository](https://github.com/netbirdio/dashboard).
|
||||
* 5 min [demo video](https://youtu.be/Tu9tPsUWaY0) on YouTube.
|
||||
- Hosted version: [https://app.netbird.io/](https://app.netbird.io/).
|
||||
- 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).
|
||||
- Step-by-step [Installation Guide](https://netbird.io/docs/getting-started/installation) for different platforms.
|
||||
- Web UI [repository](https://github.com/netbirdio/dashboard).
|
||||
- 5 min [demo video](https://youtu.be/Tu9tPsUWaY0) on YouTube.
|
||||
|
||||
|
||||
### A bit on NetBird internals
|
||||
* 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 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.
|
||||
* 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.
|
||||
* 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.
|
||||
- Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
|
||||
- Every agent connects to [Management Service](management/) that holds network state, manages peer IPs, and distributes network updates to agents (peers).
|
||||
- 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.
|
||||
- Agents negotiate a connection through [Signal Service](signal/) passing p2p encrypted messages with candidates.
|
||||
- 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.
|
||||
|
||||
@@ -105,7 +96,10 @@ For stable versions, see [releases](https://github.com/netbirdio/netbird/release
|
||||
See a complete [architecture overview](https://netbird.io/docs/overview/architecture) for details.
|
||||
|
||||
### 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)
|
||||
|
||||
### 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).
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
FROM gcr.io/distroless/base:debug
|
||||
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"]
|
||||
COPY netbird /go/bin/netbird
|
||||
@@ -43,6 +43,8 @@ var loginCmd = &cobra.Command{
|
||||
return fmt.Errorf("get config file: %v", err)
|
||||
}
|
||||
|
||||
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
|
||||
|
||||
err = foregroundLogin(ctx, cmd, config, setupKey)
|
||||
if err != nil {
|
||||
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(
|
||||
providerConfig.ProviderConfig.Audience,
|
||||
providerConfig.ProviderConfig.ClientID,
|
||||
providerConfig.ProviderConfig.Domain,
|
||||
providerConfig.ProviderConfig.TokenEndpoint,
|
||||
providerConfig.ProviderConfig.DeviceAuthEndpoint,
|
||||
)
|
||||
|
||||
flowInfo, err := hostedClient.RequestDeviceCode(context.TODO())
|
||||
|
||||
@@ -94,6 +94,7 @@ func init() {
|
||||
rootCmd.AddCommand(statusCmd)
|
||||
rootCmd.AddCommand(loginCmd)
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.AddCommand(sshCmd)
|
||||
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
|
||||
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
|
||||
}
|
||||
|
||||
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,24 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"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/spf13/cobra"
|
||||
"google.golang.org/grpc/status"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
var (
|
||||
detailFlag bool
|
||||
ipsFilter []string
|
||||
statusFilter string
|
||||
ipsFilterMap map[string]struct{}
|
||||
)
|
||||
|
||||
var statusCmd = &cobra.Command{
|
||||
@@ -20,7 +31,12 @@ var statusCmd = &cobra.Command{
|
||||
|
||||
cmd.SetOut(cmd.OutOrStdout())
|
||||
|
||||
err := util.InitLog(logLevel, "console")
|
||||
err := parseFilters()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = util.InitLog(logLevel, "console")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed initializing log %v", err)
|
||||
}
|
||||
@@ -35,21 +51,251 @@ var statusCmd = &cobra.Command{
|
||||
}
|
||||
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 {
|
||||
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) {
|
||||
|
||||
cmd.Printf("Run UP command to log in with SSO (interactive login):\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" +
|
||||
"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")
|
||||
cmd.Printf("%s\n"+
|
||||
"Run UP command to log in with SSO (interactive login):\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"+
|
||||
"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",
|
||||
daemonStatus,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
pbFullStatus := resp.GetFullStatus()
|
||||
fullStatus := fromProtoFullStatus(pbFullStatus)
|
||||
|
||||
cmd.Print(parseFullStatus(fullStatus, detailFlag, daemonStatus, resp.GetDaemonVersion()))
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
ipsFilterMap = make(map[string]struct{})
|
||||
statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information")
|
||||
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()
|
||||
|
||||
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(),
|
||||
}
|
||||
peersState = append(peersState, peerState)
|
||||
}
|
||||
|
||||
fullStatus.Peers = peersState
|
||||
|
||||
return fullStatus
|
||||
}
|
||||
|
||||
func parseFullStatus(fullStatus nbStatus.FullStatus, printDetail bool, daemonStatus string, daemonVersion string) string {
|
||||
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"
|
||||
}
|
||||
|
||||
interfaceIP := fullStatus.LocalPeerState.IP
|
||||
|
||||
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"+
|
||||
"NetBird IP: %s\n"+
|
||||
"Interface type: %s\n"+
|
||||
"Peers count: %s\n",
|
||||
daemonVersion,
|
||||
system.NetbirdVersion(),
|
||||
daemonStatus,
|
||||
managementConnString,
|
||||
managementStatusURL,
|
||||
signalConnString,
|
||||
signalStatusURL,
|
||||
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 Peer:\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.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
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -39,6 +40,8 @@ var upCmd = &cobra.Command{
|
||||
return fmt.Errorf("get config file: %v", err)
|
||||
}
|
||||
|
||||
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
|
||||
|
||||
err = foregroundLogin(ctx, cmd, config, setupKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("foreground login failed: %v", err)
|
||||
@@ -47,7 +50,7 @@ var upCmd = &cobra.Command{
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
SetupCloseHandler(ctx, cancel)
|
||||
return internal.RunClient(ctx, config)
|
||||
return internal.RunClient(ctx, config, nbStatus.NewRecorder())
|
||||
}
|
||||
|
||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||
|
||||
@@ -3,16 +3,16 @@ package internal
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/client/ssh"
|
||||
"github.com/netbirdio/netbird/iface"
|
||||
mgm "github.com/netbirdio/netbird/management/client"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/netbirdio/netbird/iface"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
var managementURLDefault *url.URL
|
||||
@@ -22,7 +22,7 @@ func ManagementURLDefault() *url.URL {
|
||||
}
|
||||
|
||||
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 {
|
||||
panic(err)
|
||||
}
|
||||
@@ -38,14 +38,20 @@ type Config struct {
|
||||
AdminURL *url.URL
|
||||
WgIface string
|
||||
IFaceBlackList []string
|
||||
// SSHKey is a private SSH key in a PEM format
|
||||
SSHKey string
|
||||
}
|
||||
|
||||
// createNewConfig creates a new config generating a new Wireguard key and saving to file
|
||||
func createNewConfig(managementURL, adminURL, configPath, preSharedKey string) (*Config, error) {
|
||||
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, IFaceBlackList: []string{}}
|
||||
if managementURL != "" {
|
||||
URL, err := parseURL("Management URL", managementURL)
|
||||
URL, err := ParseURL("Management URL", managementURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -58,10 +64,18 @@ func createNewConfig(managementURL, adminURL, configPath, preSharedKey string) (
|
||||
config.PreSharedKey = preSharedKey
|
||||
}
|
||||
|
||||
if adminURL != "" {
|
||||
newURL, err := ParseURL("Admin Panel URL", adminURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.AdminURL = newURL
|
||||
}
|
||||
|
||||
config.IFaceBlackList = []string{iface.WgInterfaceDefault, "tun0", "zt", "ZeroTier", "utun", "wg", "ts",
|
||||
"Tailscale", "tailscale"}
|
||||
|
||||
err := util.WriteJson(configPath, config)
|
||||
err = util.WriteJson(configPath, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -69,7 +83,8 @@ func createNewConfig(managementURL, adminURL, configPath, preSharedKey string) (
|
||||
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)
|
||||
if err != nil {
|
||||
log.Errorf("failed parsing management URL %s: [%s]", managementURL, err.Error())
|
||||
@@ -101,7 +116,7 @@ func ReadConfig(managementURL, adminURL, configPath string, preSharedKey *string
|
||||
if managementURL != "" && config.ManagementURL.String() != managementURL {
|
||||
log.Infof("new Management URL provided, updated to %s (old value %s)",
|
||||
managementURL, config.ManagementURL)
|
||||
newURL, err := parseURL("Management URL", managementURL)
|
||||
newURL, err := ParseURL("Management URL", managementURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -112,7 +127,7 @@ func ReadConfig(managementURL, adminURL, configPath string, preSharedKey *string
|
||||
if adminURL != "" && (config.AdminURL == nil || config.AdminURL.String() != adminURL) {
|
||||
log.Infof("new Admin Panel URL provided, updated to %s (old value %s)",
|
||||
adminURL, config.AdminURL)
|
||||
newURL, err := parseURL("Admin Panel URL", adminURL)
|
||||
newURL, err := ParseURL("Admin Panel URL", adminURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -126,6 +141,14 @@ func ReadConfig(managementURL, adminURL, configPath string, preSharedKey *string
|
||||
config.PreSharedKey = *preSharedKey
|
||||
refresh = true
|
||||
}
|
||||
if config.SSHKey == "" {
|
||||
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.SSHKey = string(pem)
|
||||
refresh = true
|
||||
}
|
||||
|
||||
if refresh {
|
||||
// since we have new management URL, we need to update config file
|
||||
@@ -174,9 +197,14 @@ type ProviderConfig struct {
|
||||
// ClientSecret An IDP application client secret
|
||||
ClientSecret string
|
||||
// Domain An IDP API domain
|
||||
// Deprecated. Use OIDCConfigEndpoint instead
|
||||
Domain string
|
||||
// Audience An Audience for to authorization validation
|
||||
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) {
|
||||
@@ -198,7 +226,13 @@ func GetDeviceAuthorizationFlowInfo(ctx context.Context, config *Config) (Device
|
||||
log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), 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()
|
||||
if err != nil {
|
||||
@@ -217,20 +251,16 @@ func GetDeviceAuthorizationFlowInfo(ctx context.Context, config *Config) (Device
|
||||
}
|
||||
}
|
||||
|
||||
err = mgmClient.Close()
|
||||
if err != nil {
|
||||
log.Errorf("failed closing Management Service client: %v", err)
|
||||
return DeviceAuthorizationFlow{}, err
|
||||
}
|
||||
|
||||
return DeviceAuthorizationFlow{
|
||||
Provider: protoDeviceAuthorizationFlow.Provider.String(),
|
||||
|
||||
ProviderConfig: ProviderConfig{
|
||||
Audience: protoDeviceAuthorizationFlow.ProviderConfig.Audience,
|
||||
ClientID: protoDeviceAuthorizationFlow.ProviderConfig.ClientID,
|
||||
ClientSecret: protoDeviceAuthorizationFlow.ProviderConfig.ClientSecret,
|
||||
Domain: protoDeviceAuthorizationFlow.ProviderConfig.Domain,
|
||||
Audience: protoDeviceAuthorizationFlow.GetProviderConfig().GetAudience(),
|
||||
ClientID: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientID(),
|
||||
ClientSecret: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientSecret(),
|
||||
Domain: protoDeviceAuthorizationFlow.GetProviderConfig().Domain,
|
||||
TokenEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetTokenEndpoint(),
|
||||
DeviceAuthEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetDeviceAuthEndpoint(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/client/ssh"
|
||||
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
@@ -15,17 +19,17 @@ import (
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
gstatus "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// 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{
|
||||
InitialInterval: time.Second,
|
||||
RandomizationFactor: backoff.DefaultRandomizationFactor,
|
||||
Multiplier: backoff.DefaultMultiplier,
|
||||
MaxInterval: 10 * time.Second,
|
||||
MaxElapsedTime: 24 * 3 * time.Hour, // stop the client after 3 days trying (must be a huge problem, e.g permission denied)
|
||||
RandomizationFactor: 1,
|
||||
Multiplier: 1.7,
|
||||
MaxInterval: 15 * time.Second,
|
||||
MaxElapsedTime: 3 * 30 * 24 * time.Hour, // 3 months
|
||||
Stop: backoff.Stop,
|
||||
Clock: backoff.SystemClock,
|
||||
}
|
||||
@@ -39,6 +43,25 @@ func RunClient(ctx context.Context, config *Config) error {
|
||||
}()
|
||||
|
||||
wrapErr := state.Wrap
|
||||
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
|
||||
if err != nil {
|
||||
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
var mgmTlsEnabled bool
|
||||
if config.ManagementURL.Scheme == "https" {
|
||||
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 {
|
||||
@@ -48,32 +71,54 @@ func RunClient(ctx context.Context, config *Config) error {
|
||||
}
|
||||
|
||||
state.Set(StatusConnecting)
|
||||
// validate our peer's Wireguard PRIVATE key
|
||||
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
|
||||
if err != nil {
|
||||
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
var mgmTlsEnabled bool
|
||||
if config.ManagementURL.Scheme == "https" {
|
||||
mgmTlsEnabled = true
|
||||
}
|
||||
|
||||
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
|
||||
mgmClient, loginResp, err := connectToManagement(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
||||
loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
||||
log.Info("peer registration required. Please run `netbird status` for details")
|
||||
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
|
||||
state.Set(StatusNeedsLogin)
|
||||
return nil
|
||||
return backoff.Permanent(wrapErr(err)) // unrecoverable error
|
||||
}
|
||||
return wrapErr(err)
|
||||
}
|
||||
statusRecorder.MarkManagementConnected(managementURL)
|
||||
|
||||
localPeerState := nbStatus.LocalPeerState{
|
||||
IP: loginResp.GetPeerConfig().GetAddress(),
|
||||
PubKey: myPrivateKey.PublicKey().String(),
|
||||
KernelInterface: iface.WireguardModExists(),
|
||||
}
|
||||
|
||||
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
|
||||
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
|
||||
@@ -81,6 +126,14 @@ func RunClient(ctx context.Context, config *Config) error {
|
||||
log.Error(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()
|
||||
|
||||
@@ -90,7 +143,7 @@ func RunClient(ctx context.Context, config *Config) error {
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig)
|
||||
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig, statusRecorder)
|
||||
err = engine.Start()
|
||||
if err != nil {
|
||||
log.Errorf("error while starting Netbird Connection Engine: %s", err)
|
||||
@@ -104,24 +157,13 @@ func RunClient(ctx context.Context, config *Config) error {
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
log.Errorf("failed stopping engine %v", err)
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
log.Info("stopped Netbird client")
|
||||
log.Info("stopped NetBird client")
|
||||
|
||||
if _, err := state.Status(); err == ErrResetConnection {
|
||||
return err
|
||||
@@ -130,9 +172,9 @@ func RunClient(ctx context.Context, config *Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := backoff.Retry(operation, backOff)
|
||||
err = backoff.Retry(operation, backOff)
|
||||
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 nil
|
||||
@@ -147,6 +189,7 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe
|
||||
IFaceBlackList: config.IFaceBlackList,
|
||||
WgPrivateKey: key,
|
||||
WgPort: iface.DefaultWgPort,
|
||||
SSHKey: []byte(config.SSHKey),
|
||||
}
|
||||
|
||||
if config.PreSharedKey != "" {
|
||||
@@ -172,33 +215,99 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig,
|
||||
signalClient, err := signal.NewClient(ctx, wtConfig.Signal.Uri, ourPrivateKey, sigTLSEnabled)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// connectToManagement 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) {
|
||||
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)
|
||||
// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Wiretrustee config (signal, turn, stun hosts, etc)
|
||||
func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte) (*mgmProto.LoginResponse, error) {
|
||||
|
||||
serverPublicKey, err := client.GetServerPublicKey()
|
||||
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)
|
||||
loginResp, err := client.Login(*serverPublicKey, sysInfo)
|
||||
loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("peer logged in to Management Service %s", managementAddr)
|
||||
|
||||
return client, loginResp, nil
|
||||
return 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
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ package internal
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
nbssh "github.com/netbirdio/netbird/client/ssh"
|
||||
nbstatus "github.com/netbirdio/netbird/client/status"
|
||||
"math/rand"
|
||||
"net"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -54,6 +58,9 @@ type EngineConfig struct {
|
||||
|
||||
// UDPMuxSrflxPort default value 0 - the system will pick an available port
|
||||
UDPMuxSrflxPort int
|
||||
|
||||
// SSHKey is a private SSH key in a PEM format
|
||||
SSHKey []byte
|
||||
}
|
||||
|
||||
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
|
||||
@@ -87,6 +94,11 @@ type Engine struct {
|
||||
|
||||
// networkSerial is the latest CurrentSerial (state ID) of the network sent by the Management service
|
||||
networkSerial uint64
|
||||
|
||||
sshServerFunc func(hostKeyPEM []byte, addr string) (nbssh.Server, error)
|
||||
sshServer nbssh.Server
|
||||
|
||||
statusRecorder *nbstatus.Status
|
||||
}
|
||||
|
||||
// Peer is an instance of the Connection Peer
|
||||
@@ -98,19 +110,22 @@ type Peer struct {
|
||||
// NewEngine creates a new Connection Engine
|
||||
func NewEngine(
|
||||
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 {
|
||||
return &Engine{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
signal: signalClient,
|
||||
mgmClient: mgmClient,
|
||||
peerConns: map[string]*peer.Conn{},
|
||||
syncMsgMux: &sync.Mutex{},
|
||||
config: config,
|
||||
STUNs: []*ice.URL{},
|
||||
TURNs: []*ice.URL{},
|
||||
networkSerial: 0,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
signal: signalClient,
|
||||
mgmClient: mgmClient,
|
||||
peerConns: map[string]*peer.Conn{},
|
||||
syncMsgMux: &sync.Mutex{},
|
||||
config: config,
|
||||
STUNs: []*ice.URL{},
|
||||
TURNs: []*ice.URL{},
|
||||
networkSerial: 0,
|
||||
sshServerFunc: nbssh.DefaultSSHServer,
|
||||
statusRecorder: statusRecorder,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +175,13 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("stopped Netbird Engine")
|
||||
|
||||
return nil
|
||||
@@ -283,9 +305,21 @@ func (e *Engine) removeAllPeers() error {
|
||||
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 {
|
||||
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]
|
||||
if exists {
|
||||
delete(e.peerConns, peerKey)
|
||||
@@ -348,8 +382,6 @@ func signalCandidate(candidate ice.Candidate, myKey wgtypes.Key, remoteKey wgtyp
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("failed signaling candidate to the remote peer %s %s", remoteKey.String(), err)
|
||||
// todo ??
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -398,12 +430,6 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
||||
}
|
||||
|
||||
if update.GetNetworkMap() != nil {
|
||||
if update.GetNetworkMap().GetPeerConfig() != nil {
|
||||
err := e.updateConfig(update.GetNetworkMap().GetPeerConfig())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// only apply new changes and ignore old ones
|
||||
err := e.updateNetworkMap(update.GetNetworkMap())
|
||||
if err != nil {
|
||||
@@ -414,6 +440,53 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
||||
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()
|
||||
@@ -422,9 +495,17 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -486,6 +567,15 @@ func (e *Engine) updateTURNs(turns []*mgmProto.ProtectedHostConfig) 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()
|
||||
if e.networkSerial > serial {
|
||||
log.Debugf("received outdated NetworkMap with serial %d, ignoring", serial)
|
||||
@@ -515,6 +605,18 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e.networkSerial = serial
|
||||
@@ -543,12 +645,17 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
|
||||
}
|
||||
e.peerConns[peerKey] = conn
|
||||
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e Engine) connWorker(conn *peer.Conn, peerKey string) {
|
||||
func (e *Engine) connWorker(conn *peer.Conn, peerKey string) {
|
||||
for {
|
||||
|
||||
// randomize starting time a bit
|
||||
@@ -567,6 +674,13 @@ func (e Engine) connWorker(conn *peer.Conn, peerKey string) {
|
||||
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()
|
||||
if err != nil {
|
||||
log.Debugf("connection to peer %s failed: %v", peerKey, err)
|
||||
@@ -588,6 +702,7 @@ func (e Engine) peerExists(peerKey string) bool {
|
||||
}
|
||||
|
||||
func (e Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, error) {
|
||||
log.Debugf("creating peer connection %s", pubKey)
|
||||
var stunTurn []*ice.URL
|
||||
stunTurn = append(stunTurn, e.STUNs...)
|
||||
stunTurn = append(stunTurn, e.TURNs...)
|
||||
@@ -613,7 +728,7 @@ func (e Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, er
|
||||
ProxyConfig: proxyConfig,
|
||||
}
|
||||
|
||||
peerConn, err := peer.NewConn(config)
|
||||
peerConn, err := peer.NewConn(config, e.statusRecorder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ package internal
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/client/ssh"
|
||||
nbstatus "github.com/netbirdio/netbird/client/status"
|
||||
"github.com/netbirdio/netbird/iface"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -40,6 +44,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) {
|
||||
// test setup
|
||||
key, err := wgtypes.GeneratePrivateKey()
|
||||
@@ -52,11 +190,12 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
||||
WgIfaceName: "utun100",
|
||||
WgIfaceName: "utun102",
|
||||
WgAddr: "100.64.0.1/24",
|
||||
WgPrivateKey: key,
|
||||
WgPort: 33100,
|
||||
})
|
||||
}, nbstatus.NewRecorder())
|
||||
engine.wgInterface, err = iface.NewWGIFace("utun102", "100.64.0.1/24", iface.DefaultMTU)
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
@@ -231,11 +370,11 @@ func TestEngine_Sync(t *testing.T) {
|
||||
}
|
||||
|
||||
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{SyncFunc: syncFunc}, &EngineConfig{
|
||||
WgIfaceName: "utun100",
|
||||
WgIfaceName: "utun103",
|
||||
WgAddr: "100.64.0.1/24",
|
||||
WgPrivateKey: key,
|
||||
WgPort: 33100,
|
||||
})
|
||||
}, nbstatus.NewRecorder())
|
||||
|
||||
defer func() {
|
||||
err := engine.Stop()
|
||||
@@ -418,7 +557,7 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin
|
||||
}
|
||||
|
||||
info := system.GetInfo(ctx)
|
||||
resp, err := mgmtClient.Register(*publicKey, setupKey, "", info)
|
||||
resp, err := mgmtClient.Register(*publicKey, setupKey, "", info, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -438,7 +577,7 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin
|
||||
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) {
|
||||
|
||||
@@ -2,8 +2,8 @@ package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/netbirdio/netbird/client/ssh"
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
mgm "github.com/netbirdio/netbird/management/client"
|
||||
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||
@@ -26,13 +26,19 @@ func Login(ctx context.Context, config *Config, setupKey string, jwtToken string
|
||||
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)
|
||||
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
|
||||
}
|
||||
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()
|
||||
if err != nil {
|
||||
@@ -40,15 +46,20 @@ func Login(ctx context.Context, config *Config, setupKey string, jwtToken string
|
||||
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 {
|
||||
log.Errorf("failed logging-in peer on Management Service : %v", err)
|
||||
return err
|
||||
}
|
||||
log.Infof("peer has successfully logged-in to the Management service %s", config.ManagementURL.String())
|
||||
|
||||
err = mgmClient.Close()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -56,26 +67,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.
|
||||
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)
|
||||
loginResp, err := client.Login(serverPublicKey, sysInfo)
|
||||
loginResp, err := client.Login(serverPublicKey, sysInfo, pubSSHKey)
|
||||
if err != nil {
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
||||
log.Debugf("peer registration required")
|
||||
return registerPeer(ctx, serverPublicKey, client, setupKey, jwtToken)
|
||||
return registerPeer(ctx, serverPublicKey, client, setupKey, jwtToken, pubSSHKey)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("peer has successfully logged-in to Management Service")
|
||||
|
||||
return loginResp, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
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)
|
||||
if err != nil && jwtToken == "" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid setup-key or no sso information provided, err: %v", err)
|
||||
@@ -83,7 +92,7 @@ func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.
|
||||
|
||||
log.Debugf("sending peer registration request to Management Service")
|
||||
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 {
|
||||
log.Errorf("failed registering peer %v,%s", err, validSetupKey.String())
|
||||
return nil, err
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -14,7 +16,6 @@ import (
|
||||
// OAuthClient is a OAuth client interface for various idp providers
|
||||
type OAuthClient interface {
|
||||
RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
|
||||
RotateAccessToken(ctx context.Context, refreshToken string) (TokenInfo, error)
|
||||
WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error)
|
||||
GetClientID(ctx context.Context) string
|
||||
}
|
||||
@@ -55,8 +56,10 @@ type Hosted struct {
|
||||
Audience string
|
||||
// Hosted Native application client id
|
||||
ClientID string
|
||||
// Hosted domain
|
||||
Domain string
|
||||
// TokenEndpoint to request access token
|
||||
TokenEndpoint string
|
||||
// DeviceAuthEndpoint to request device authorization code
|
||||
DeviceAuthEndpoint string
|
||||
|
||||
HTTPClient HTTPClient
|
||||
}
|
||||
@@ -84,11 +87,11 @@ type TokenRequestResponse struct {
|
||||
|
||||
// Claims used when validating the access token
|
||||
type Claims struct {
|
||||
Audience string `json:"aud"`
|
||||
Audience interface{} `json:"aud"`
|
||||
}
|
||||
|
||||
// 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.MaxIdleConns = 5
|
||||
|
||||
@@ -98,10 +101,11 @@ func NewHostedDeviceFlow(audience string, clientID string, domain string) *Hoste
|
||||
}
|
||||
|
||||
return &Hosted{
|
||||
Audience: audience,
|
||||
ClientID: clientID,
|
||||
Domain: domain,
|
||||
HTTPClient: httpClient,
|
||||
Audience: audience,
|
||||
ClientID: clientID,
|
||||
TokenEndpoint: tokenEndpoint,
|
||||
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
|
||||
func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error) {
|
||||
url := "https://" + h.Domain + "/oauth/device/code"
|
||||
codePayload := RequestDeviceCodePayload{
|
||||
Audience: h.Audience,
|
||||
ClientID: h.ClientID,
|
||||
}
|
||||
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)
|
||||
form := url.Values{}
|
||||
form.Add("client_id", h.ClientID)
|
||||
form.Add("audience", h.Audience)
|
||||
req, err := http.NewRequest("POST", h.DeviceAuthEndpoint,
|
||||
strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return DeviceAuthInfo{}, fmt.Errorf("creating request failed with error: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Add("content-type", "application/json")
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
res, err := h.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -135,7 +132,7 @@ func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
// 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) {
|
||||
@@ -163,24 +202,8 @@ func (h *Hosted) WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo,
|
||||
case <-ctx.Done():
|
||||
return TokenInfo{}, ctx.Err()
|
||||
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)
|
||||
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)
|
||||
tokenResponse, err := h.requestToken(info)
|
||||
if err != nil {
|
||||
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
|
||||
func isValidAccessToken(token string, audience string) error {
|
||||
if token == "" {
|
||||
@@ -297,9 +255,24 @@ func isValidAccessToken(token string, audience string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if claims.Audience != audience {
|
||||
return fmt.Errorf("invalid audience")
|
||||
if claims.Audience == nil {
|
||||
return fmt.Errorf("required token field audience is absent")
|
||||
}
|
||||
|
||||
return nil
|
||||
// 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
|
||||
}
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -24,7 +24,7 @@ type mockHTTPClient struct {
|
||||
}
|
||||
|
||||
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 {
|
||||
c.reqBody = string(body)
|
||||
}
|
||||
@@ -33,13 +33,13 @@ func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||
c.count++
|
||||
return &http.Response{
|
||||
StatusCode: c.code,
|
||||
Body: ioutil.NopCloser(strings.NewReader(c.countResBody)),
|
||||
Body: io.NopCloser(strings.NewReader(c.countResBody)),
|
||||
}, c.err
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: c.code,
|
||||
Body: ioutil.NopCloser(strings.NewReader(c.resBody)),
|
||||
Body: io.NopCloser(strings.NewReader(c.resBody)),
|
||||
}, c.err
|
||||
}
|
||||
|
||||
@@ -54,15 +54,19 @@ func TestHosted_RequestDeviceCode(t *testing.T) {
|
||||
testingFunc require.ComparisonAssertionFunc
|
||||
expectedOut DeviceAuthInfo
|
||||
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{
|
||||
name: "Payload Is Valid",
|
||||
expectPayload: RequestDeviceCodePayload{
|
||||
Audience: "ok",
|
||||
ClientID: "bla",
|
||||
},
|
||||
name: "Payload Is Valid",
|
||||
expectPayload: expectPayload,
|
||||
inputReqCode: 200,
|
||||
testingErrFunc: require.Error,
|
||||
testingFunc: require.EqualValues,
|
||||
@@ -74,6 +78,7 @@ func TestHosted_RequestDeviceCode(t *testing.T) {
|
||||
testingErrFunc: require.Error,
|
||||
expectedErrorMSG: "should return error",
|
||||
testingFunc: require.EqualValues,
|
||||
expectPayload: expectPayload,
|
||||
}
|
||||
|
||||
testCase3 := test{
|
||||
@@ -82,15 +87,13 @@ func TestHosted_RequestDeviceCode(t *testing.T) {
|
||||
testingErrFunc: require.Error,
|
||||
expectedErrorMSG: "should return error",
|
||||
testingFunc: require.EqualValues,
|
||||
expectPayload: expectPayload,
|
||||
}
|
||||
testCase4Out := DeviceAuthInfo{ExpiresIn: 10}
|
||||
testCase4 := test{
|
||||
name: "Got Device Code",
|
||||
inputResBody: fmt.Sprintf("{\"expires_in\":%d}", testCase4Out.ExpiresIn),
|
||||
expectPayload: RequestDeviceCodePayload{
|
||||
Audience: "ok",
|
||||
ClientID: "bla",
|
||||
},
|
||||
name: "Got Device Code",
|
||||
inputResBody: fmt.Sprintf("{\"expires_in\":%d}", testCase4Out.ExpiresIn),
|
||||
expectPayload: expectPayload,
|
||||
inputReqCode: 200,
|
||||
testingErrFunc: require.NoError,
|
||||
testingFunc: require.EqualValues,
|
||||
@@ -108,18 +111,17 @@ func TestHosted_RequestDeviceCode(t *testing.T) {
|
||||
}
|
||||
|
||||
hosted := Hosted{
|
||||
Audience: testCase.expectPayload.Audience,
|
||||
ClientID: testCase.expectPayload.ClientID,
|
||||
Domain: "test.hosted.com",
|
||||
HTTPClient: &httpClient,
|
||||
Audience: expectedAudience,
|
||||
ClientID: expectedClientID,
|
||||
TokenEndpoint: "test.hosted.com/token",
|
||||
DeviceAuthEndpoint: "test.hosted.com/device/auth",
|
||||
HTTPClient: &httpClient,
|
||||
}
|
||||
|
||||
authInfo, err := hosted.RequestDeviceCode(context.TODO())
|
||||
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
|
||||
|
||||
payload, _ := json.Marshal(testCase.expectPayload)
|
||||
|
||||
require.EqualValues(t, string(payload), httpClient.reqBody, "payload should match")
|
||||
require.EqualValues(t, expectPayload, httpClient.reqBody, "payload should match")
|
||||
|
||||
testCase.testingFunc(t, testCase.expectedOut, authInfo, testCase.expectedMSG)
|
||||
|
||||
@@ -143,7 +145,7 @@ func TestHosted_WaitToken(t *testing.T) {
|
||||
testingFunc require.ComparisonAssertionFunc
|
||||
expectedOut TokenInfo
|
||||
expectedMSG string
|
||||
expectPayload TokenRequestPayload
|
||||
expectPayload string
|
||||
}
|
||||
|
||||
defaultInfo := DeviceAuthInfo{
|
||||
@@ -152,11 +154,13 @@ func TestHosted_WaitToken(t *testing.T) {
|
||||
Interval: 1,
|
||||
}
|
||||
|
||||
tokenReqPayload := TokenRequestPayload{
|
||||
GrantType: HostedGrantType,
|
||||
DeviceCode: defaultInfo.DeviceCode,
|
||||
ClientID: "test",
|
||||
}
|
||||
clientID := "test"
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("grant_type", HostedGrantType)
|
||||
form.Add("device_code", defaultInfo.DeviceCode)
|
||||
form.Add("client_id", clientID)
|
||||
tokenReqPayload := form.Encode()
|
||||
|
||||
testCase1 := test{
|
||||
name: "Payload Is Valid",
|
||||
@@ -268,10 +272,11 @@ func TestHosted_WaitToken(t *testing.T) {
|
||||
}
|
||||
|
||||
hosted := Hosted{
|
||||
Audience: testCase.inputAudience,
|
||||
ClientID: testCase.expectPayload.ClientID,
|
||||
Domain: "test.hosted.com",
|
||||
HTTPClient: &httpClient,
|
||||
Audience: testCase.inputAudience,
|
||||
ClientID: clientID,
|
||||
TokenEndpoint: "test.hosted.com/token",
|
||||
DeviceAuthEndpoint: "test.hosted.com/device/auth",
|
||||
HTTPClient: &httpClient,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), testCase.inputTimeout)
|
||||
@@ -279,12 +284,7 @@ func TestHosted_WaitToken(t *testing.T) {
|
||||
tokenInfo, err := hosted.WaitToken(ctx, testCase.inputInfo)
|
||||
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")
|
||||
require.EqualValues(t, testCase.expectPayload, httpClient.reqBody, "payload should match")
|
||||
|
||||
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,6 +2,7 @@ package peer
|
||||
|
||||
import (
|
||||
"context"
|
||||
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||
"github.com/netbirdio/netbird/iface"
|
||||
"golang.zx2c4.com/wireguard/wgctrl"
|
||||
"net"
|
||||
@@ -64,6 +65,8 @@ type Conn struct {
|
||||
agent *ice.Agent
|
||||
status ConnStatus
|
||||
|
||||
statusRecorder *nbStatus.Status
|
||||
|
||||
proxy proxy.Proxy
|
||||
}
|
||||
|
||||
@@ -72,9 +75,14 @@ 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.
|
||||
// To establish a connection run Conn.Open
|
||||
func NewConn(config ConnConfig) (*Conn, error) {
|
||||
func NewConn(config ConnConfig, statusRecorder *nbStatus.Status) (*Conn, error) {
|
||||
return &Conn{
|
||||
config: config,
|
||||
mu: sync.Mutex{},
|
||||
@@ -82,6 +90,7 @@ func NewConn(config ConnConfig) (*Conn, error) {
|
||||
closeCh: make(chan struct{}),
|
||||
remoteOffersCh: make(chan IceCredentials),
|
||||
remoteAnswerCh: make(chan IceCredentials),
|
||||
statusRecorder: statusRecorder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -157,6 +166,17 @@ func (conn *Conn) reCreateAgent() error {
|
||||
func (conn *Conn) Open() error {
|
||||
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() {
|
||||
err := conn.cleanup()
|
||||
if err != nil {
|
||||
@@ -165,7 +185,7 @@ func (conn *Conn) Open() error {
|
||||
}
|
||||
}()
|
||||
|
||||
err := conn.reCreateAgent()
|
||||
err = conn.reCreateAgent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -205,6 +225,15 @@ func (conn *Conn) Open() error {
|
||||
defer conn.notifyDisconnected()
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -224,7 +253,7 @@ func (conn *Conn) Open() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// the connection has been established successfully so we are ready to start the proxy
|
||||
// the ice connection has been established successfully so we are ready to start the proxy
|
||||
err = conn.startProxy(remoteConn)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -259,6 +288,10 @@ func shouldUseProxy(pair *ice.CandidatePair) bool {
|
||||
remoteIsPublic := IsPublicIP(remoteIP)
|
||||
myIsPublic := IsPublicIP(myIp)
|
||||
|
||||
if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay {
|
||||
return true
|
||||
}
|
||||
|
||||
//one of the hosts has a public IP
|
||||
if remoteIsPublic && pair.Remote.Type() == ice.CandidateTypeHost {
|
||||
return false
|
||||
@@ -296,12 +329,15 @@ func (conn *Conn) startProxy(remoteConn net.Conn) error {
|
||||
return err
|
||||
}
|
||||
|
||||
peerState := nbStatus.PeerState{PubKey: conn.config.Key}
|
||||
useProxy := shouldUseProxy(pair)
|
||||
var p proxy.Proxy
|
||||
if useProxy {
|
||||
p = proxy.NewWireguardProxy(conn.config.ProxyConfig)
|
||||
peerState.Direct = false
|
||||
} else {
|
||||
p = proxy.NewNoProxy(conn.config.ProxyConfig)
|
||||
peerState.Direct = true
|
||||
}
|
||||
conn.proxy = p
|
||||
err = p.Start(remoteConn)
|
||||
@@ -311,6 +347,19 @@ func (conn *Conn) startProxy(remoteConn net.Conn) error {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -343,6 +392,17 @@ func (conn *Conn) cleanup() error {
|
||||
|
||||
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)
|
||||
|
||||
return nil
|
||||
@@ -367,7 +427,7 @@ func (conn *Conn) SetSignalCandidate(handler func(candidate ice.Candidate) error
|
||||
// and then signals them to the remote peer
|
||||
func (conn *Conn) onICECandidate(candidate ice.Candidate) {
|
||||
if candidate != nil {
|
||||
// log.Debugf("discovered local candidate %s", candidate.String())
|
||||
log.Debugf("discovered local candidate %s", candidate.String())
|
||||
go func() {
|
||||
err := conn.signalCandidate(candidate)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package peer
|
||||
import (
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/netbirdio/netbird/client/internal/proxy"
|
||||
nbstatus "github.com/netbirdio/netbird/client/status"
|
||||
"github.com/netbirdio/netbird/iface"
|
||||
"github.com/pion/ice/v2"
|
||||
"sync"
|
||||
@@ -32,7 +33,7 @@ func TestNewConn_interfaceFilter(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConn_GetKey(t *testing.T) {
|
||||
conn, err := NewConn(connConf)
|
||||
conn, err := NewConn(connConf, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -44,7 +45,7 @@ func TestConn_GetKey(t *testing.T) {
|
||||
|
||||
func TestConn_OnRemoteOffer(t *testing.T) {
|
||||
|
||||
conn, err := NewConn(connConf)
|
||||
conn, err := NewConn(connConf, nbstatus.NewRecorder())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -74,7 +75,7 @@ func TestConn_OnRemoteOffer(t *testing.T) {
|
||||
|
||||
func TestConn_OnRemoteAnswer(t *testing.T) {
|
||||
|
||||
conn, err := NewConn(connConf)
|
||||
conn, err := NewConn(connConf, nbstatus.NewRecorder())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -103,7 +104,7 @@ func TestConn_OnRemoteAnswer(t *testing.T) {
|
||||
}
|
||||
func TestConn_Status(t *testing.T) {
|
||||
|
||||
conn, err := NewConn(connConf)
|
||||
conn, err := NewConn(connConf, nbstatus.NewRecorder())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -130,7 +131,7 @@ func TestConn_Status(t *testing.T) {
|
||||
|
||||
func TestConn_Close(t *testing.T) {
|
||||
|
||||
conn, err := NewConn(connConf)
|
||||
conn, err := NewConn(connConf, nbstatus.NewRecorder())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ type ConnStatus int
|
||||
func (s ConnStatus) String() string {
|
||||
switch s {
|
||||
case StatusConnecting:
|
||||
return "StatusConnecting"
|
||||
return "Connecting"
|
||||
case StatusConnected:
|
||||
return "StatusConnected"
|
||||
return "Connected"
|
||||
case StatusDisconnected:
|
||||
return "StatusDisconnected"
|
||||
return "Disconnected"
|
||||
default:
|
||||
log.Errorf("unknown status: %d", s)
|
||||
return "INVALID_PEER_CONNECTION_STATUS"
|
||||
@@ -19,7 +19,7 @@ func (s ConnStatus) String() string {
|
||||
}
|
||||
|
||||
const (
|
||||
StatusConnected = iota
|
||||
StatusConnected ConnStatus = iota
|
||||
StatusConnecting
|
||||
StatusDisconnected
|
||||
)
|
||||
|
||||
@@ -12,9 +12,9 @@ func TestConnStatus_String(t *testing.T) {
|
||||
status ConnStatus
|
||||
want string
|
||||
}{
|
||||
{"StatusConnected", StatusConnected, "StatusConnected"},
|
||||
{"StatusDisconnected", StatusDisconnected, "StatusDisconnected"},
|
||||
{"StatusConnecting", StatusConnecting, "StatusConnecting"},
|
||||
{"StatusConnected", StatusConnected, "Connected"},
|
||||
{"StatusDisconnected", StatusDisconnected, "Disconnected"},
|
||||
{"StatusConnecting", StatusConnecting, "Connecting"},
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/netbirdio/netbird/client/cmd"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.19.4
|
||||
// protoc v3.12.4
|
||||
// source: daemon.proto
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
_ "github.com/golang/protobuf/protoc-gen-go/descriptor"
|
||||
timestamp "github.com/golang/protobuf/ptypes/timestamp"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
_ "google.golang.org/protobuf/types/descriptorpb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
@@ -332,6 +333,8 @@ type StatusRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
GetFullPeerStatus bool `protobuf:"varint,1,opt,name=getFullPeerStatus,proto3" json:"getFullPeerStatus,omitempty"`
|
||||
}
|
||||
|
||||
func (x *StatusRequest) Reset() {
|
||||
@@ -366,13 +369,23 @@ func (*StatusRequest) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *StatusRequest) GetGetFullPeerStatus() bool {
|
||||
if x != nil {
|
||||
return x.GetFullPeerStatus
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type StatusResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// 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() {
|
||||
@@ -414,6 +427,20 @@ func (x *StatusResponse) GetStatus() string {
|
||||
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 {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -612,58 +639,470 @@ func (x *GetConfigResponse) GetAdminURL() string {
|
||||
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 *timestamp.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"`
|
||||
}
|
||||
|
||||
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() *timestamp.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 ""
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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_rawDesc = []byte{
|
||||
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,
|
||||
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,
|
||||
0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x74,
|
||||
0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x74,
|
||||
0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72,
|
||||
0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65,
|
||||
0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e,
|
||||
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12,
|
||||
0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x22, 0xb5, 0x01, 0x0a, 0x0d,
|
||||
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a,
|
||||
0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f,
|
||||
0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 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,
|
||||
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
|
||||
0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x90, 0x01, 0x0a, 0x0c, 0x4c, 0x6f,
|
||||
0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65,
|
||||
0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65,
|
||||
0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61,
|
||||
0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72,
|
||||
0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61,
|
||||
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c,
|
||||
0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x22, 0xb5, 0x01, 0x0a,
|
||||
0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24,
|
||||
0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c,
|
||||
0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
|
||||
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,
|
||||
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, 0x6c,
|
||||
0x65, 0x74, 0x65, 0x22, 0x31, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f,
|
||||
0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73,
|
||||
0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73,
|
||||
0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53,
|
||||
0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b,
|
||||
0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55,
|
||||
0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x53, 0x74, 0x61,
|
||||
0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x28, 0x0a, 0x0e, 0x53, 0x74,
|
||||
0x6c, 0x65, 0x74, 0x65, 0x22, 0x31, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c,
|
||||
0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75,
|
||||
0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75,
|
||||
0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53,
|
||||
0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
||||
0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a,
|
||||
0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74,
|
||||
0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67,
|
||||
0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
|
||||
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,
|
||||
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,
|
||||
0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb3, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43,
|
||||
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a,
|
||||
0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
|
||||
0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c,
|
||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46,
|
||||
0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a,
|
||||
0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65,
|
||||
0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x32, 0xf7, 0x02,
|
||||
0x61, 0x74, 0x75, 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74,
|
||||
0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
|
||||
0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75,
|
||||
0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d,
|
||||
0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d,
|
||||
0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a,
|
||||
0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a,
|
||||
0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||
0x74, 0x22, 0xb3, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67,
|
||||
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
|
||||
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a,
|
||||
0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a,
|
||||
0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
|
||||
0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68,
|
||||
0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70,
|
||||
0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61,
|
||||
0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61,
|
||||
0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x22, 0xbb, 0x02, 0x0a, 0x09, 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, 0x1e, 0x0a,
|
||||
0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a,
|
||||
0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74,
|
||||
0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
|
||||
0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55,
|
||||
0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64,
|
||||
0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12,
|
||||
0x16, 0x0a, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52,
|
||||
0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c,
|
||||
0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65,
|
||||
0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65,
|
||||
0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a,
|
||||
0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64,
|
||||
0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72,
|
||||
0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74,
|
||||
0x65, 0x54, 0x79, 0x70, 0x65, 0x22, 0x62, 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, 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,
|
||||
@@ -703,7 +1142,7 @@ func file_daemon_proto_rawDescGZIP() []byte {
|
||||
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{}{
|
||||
(*LoginRequest)(nil), // 0: daemon.LoginRequest
|
||||
(*LoginResponse)(nil), // 1: daemon.LoginResponse
|
||||
@@ -717,25 +1156,37 @@ var file_daemon_proto_goTypes = []interface{}{
|
||||
(*DownResponse)(nil), // 9: daemon.DownResponse
|
||||
(*GetConfigRequest)(nil), // 10: daemon.GetConfigRequest
|
||||
(*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
|
||||
(*timestamp.Timestamp)(nil), // 17: google.protobuf.Timestamp
|
||||
}
|
||||
var file_daemon_proto_depIdxs = []int32{
|
||||
0, // 0: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
|
||||
2, // 1: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
|
||||
4, // 2: daemon.DaemonService.Up:input_type -> daemon.UpRequest
|
||||
6, // 3: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
|
||||
8, // 4: daemon.DaemonService.Down:input_type -> daemon.DownRequest
|
||||
10, // 5: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
|
||||
1, // 6: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
|
||||
3, // 7: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
|
||||
5, // 8: daemon.DaemonService.Up:output_type -> daemon.UpResponse
|
||||
7, // 9: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
|
||||
9, // 10: daemon.DaemonService.Down:output_type -> daemon.DownResponse
|
||||
11, // 11: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
|
||||
6, // [6:12] is the sub-list for method output_type
|
||||
0, // [0:6] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
16, // 0: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
|
||||
17, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
|
||||
15, // 2: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
|
||||
14, // 3: daemon.FullStatus.signalState:type_name -> daemon.SignalState
|
||||
13, // 4: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState
|
||||
12, // 5: daemon.FullStatus.peers:type_name -> daemon.PeerState
|
||||
0, // 6: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
|
||||
2, // 7: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
|
||||
4, // 8: daemon.DaemonService.Up:input_type -> daemon.UpRequest
|
||||
6, // 9: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
|
||||
8, // 10: daemon.DaemonService.Down:input_type -> daemon.DownRequest
|
||||
10, // 11: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
|
||||
1, // 12: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
|
||||
3, // 13: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
|
||||
5, // 14: daemon.DaemonService.Up:output_type -> daemon.UpResponse
|
||||
7, // 15: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
|
||||
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() }
|
||||
@@ -888,6 +1339,66 @@ func file_daemon_proto_init() {
|
||||
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{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
@@ -895,7 +1406,7 @@ func file_daemon_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_daemon_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 12,
|
||||
NumMessages: 17,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/protobuf/descriptor.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option go_package = "/proto";
|
||||
|
||||
@@ -59,11 +60,16 @@ message UpRequest {}
|
||||
|
||||
message UpResponse {}
|
||||
|
||||
message StatusRequest{}
|
||||
message StatusRequest{
|
||||
bool getFullPeerStatus = 1;
|
||||
}
|
||||
|
||||
message StatusResponse{
|
||||
// status of the server.
|
||||
string status = 1;
|
||||
FullStatus fullStatus = 2;
|
||||
// NetBird daemon version
|
||||
string daemonVersion = 3;
|
||||
}
|
||||
|
||||
message DownRequest {}
|
||||
@@ -88,3 +94,41 @@ message GetConfigResponse {
|
||||
// adminURL settings value.
|
||||
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;
|
||||
}
|
||||
|
||||
// LocalPeerState contains the latest state of the local peer
|
||||
message LocalPeerState {
|
||||
string IP = 1;
|
||||
string pubKey = 2;
|
||||
bool kernelInterface =3;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -3,6 +3,9 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -31,6 +34,8 @@ type Server struct {
|
||||
mutex sync.Mutex
|
||||
config *internal.Config
|
||||
proto.UnimplementedDaemonServiceServer
|
||||
|
||||
statusRecorder *nbStatus.Status
|
||||
}
|
||||
|
||||
type oauthAuthFlow struct {
|
||||
@@ -52,6 +57,8 @@ func New(ctx context.Context, managementURL, adminURL, configPath, logFile strin
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
state := internal.CtxGetState(s.rootCtx)
|
||||
|
||||
// if current state contains any error, return it
|
||||
@@ -86,11 +93,16 @@ func (s *Server) Start() error {
|
||||
}
|
||||
|
||||
// if configuration exists, we just start connections.
|
||||
config, _ = internal.UpdateOldManagementPort(ctx, config, s.configPath)
|
||||
|
||||
s.config = config
|
||||
|
||||
if s.statusRecorder == nil {
|
||||
s.statusRecorder = nbStatus.NewRecorder()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
@@ -158,6 +170,12 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
|
||||
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.config = config
|
||||
s.mutex.Unlock()
|
||||
@@ -190,7 +208,8 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
|
||||
hostedClient := internal.NewHostedDeviceFlow(
|
||||
providerConfig.ProviderConfig.Audience,
|
||||
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()) {
|
||||
@@ -303,6 +322,10 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
s.oauthAuthFlow.expiresAt = time.Now()
|
||||
s.mutex.Unlock()
|
||||
|
||||
if loginStatus, err := s.loginAttempt(ctx, "", tokenInfo.AccessToken); err != nil {
|
||||
state.Set(loginStatus)
|
||||
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")
|
||||
}
|
||||
|
||||
if s.statusRecorder == nil {
|
||||
s.statusRecorder = nbStatus.NewRecorder()
|
||||
}
|
||||
|
||||
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))
|
||||
return
|
||||
}
|
||||
@@ -371,7 +398,7 @@ func (s *Server) Down(ctx context.Context, msg *proto.DownRequest) (*proto.DownR
|
||||
|
||||
// Status starts engine work in the daemon.
|
||||
func (s *Server) Status(
|
||||
ctx context.Context,
|
||||
_ context.Context,
|
||||
msg *proto.StatusRequest,
|
||||
) (*proto.StatusResponse, error) {
|
||||
s.mutex.Lock()
|
||||
@@ -382,7 +409,19 @@ func (s *Server) Status(
|
||||
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.
|
||||
@@ -418,3 +457,37 @@ func (s *Server) GetConfig(ctx context.Context, msg *proto.GetConfigRequest) (*p
|
||||
PreSharedKey: preSharedKey,
|
||||
}, 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
|
||||
|
||||
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,
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -2,83 +2,249 @@ package ssh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/creack/pty"
|
||||
"github.com/gliderlabs/ssh"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
listener net.Listener
|
||||
allowedKeys map[string]ssh.PublicKey
|
||||
mu sync.Mutex
|
||||
hostKeyPEM []byte
|
||||
// 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)
|
||||
}
|
||||
|
||||
// NewSSHServer creates new server with provided host key
|
||||
func NewSSHServer(hostKeyPEM []byte) (*Server, error) {
|
||||
ln, err := net.Listen("tcp", ":2222")
|
||||
// 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
|
||||
}
|
||||
return &Server{listener: ln, mu: sync.Mutex{}, hostKeyPEM: hostKeyPEM}, nil
|
||||
allowedKeys := make(map[string]ssh.PublicKey)
|
||||
return &DefaultServer{listener: ln, mu: sync.Mutex{}, hostKeyPEM: hostKeyPEM, authorizedKeys: allowedKeys, sessions: make([]ssh.Session, 0)}, nil
|
||||
}
|
||||
|
||||
func (srv *Server) UpdateKeys(newKeys []string) error {
|
||||
// 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()
|
||||
|
||||
srv.allowedKeys = make(map[string]ssh.PublicKey, len(newKeys))
|
||||
for _, strKey := range newKeys {
|
||||
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(strKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.allowedKeys[strKey] = parsedKey
|
||||
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. Blocking
|
||||
func (srv *Server) Stop() error {
|
||||
// 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
|
||||
}
|
||||
|
||||
// Start starts SSH server. Blocking
|
||||
func (srv *Server) Start() error {
|
||||
handler := func(s ssh.Session) {
|
||||
authorizedKey := gossh.MarshalAuthorizedKey(s.PublicKey())
|
||||
io.WriteString(s, fmt.Sprintf("public key used by %s:\n", s.User()))
|
||||
s.Write(authorizedKey)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
publicKeyOption := ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
srv.mu.Lock()
|
||||
defer srv.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
k := strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key)))
|
||||
if allowed, ok := srv.allowedKeys[k]; ok {
|
||||
if ssh.KeysEqual(allowed, key) {
|
||||
return true
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
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, handler, publicKeyOption, 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)))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// GeneratePrivateKey creates RSA Private Key of specified byte size
|
||||
func GeneratePrivateKey(bitSize int) (*rsa.PrivateKey, error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, bitSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = privateKey.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
// GeneratePublicKey takes a rsa.PublicKey and return bytes suitable for writing to .pub file
|
||||
// returns the key in format format "ssh-rsa ..."
|
||||
func GeneratePublicKey(privateKey *rsa.PublicKey) ([]byte, error) {
|
||||
publicRsaKey, err := gossh.NewPublicKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gossh.MarshalAuthorizedKey(publicRsaKey), nil
|
||||
}
|
||||
|
||||
// EncodePrivateKeyToPEM encodes Private Key from RSA to PEM format
|
||||
func EncodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte {
|
||||
// Get ASN.1 DER format
|
||||
privDER := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
|
||||
// pem.Block
|
||||
privBlock := pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Headers: nil,
|
||||
Bytes: privDER,
|
||||
}
|
||||
|
||||
// Private key in PEM format
|
||||
privatePEM := pem.EncodeToMemory(&privBlock)
|
||||
|
||||
return privatePEM
|
||||
}
|
||||
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) {
|
||||
|
||||
}
|
||||
191
client/status/status.go
Normal file
191
client/status/status.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PeerState contains the latest state of a peer
|
||||
type PeerState struct {
|
||||
IP string
|
||||
PubKey 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
|
||||
}
|
||||
|
||||
// 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
|
||||
signal SignalState
|
||||
management ManagementState
|
||||
localPeer LocalPeerState
|
||||
}
|
||||
|
||||
// NewRecorder returns a new Status instance
|
||||
func NewRecorder() *Status {
|
||||
return &Status{
|
||||
peers: make(map[string]PeerState),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
185
client/status/status_test.go
Normal file
185
client/status/status_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
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 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 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"
|
||||
"context"
|
||||
"fmt"
|
||||
"golang.org/x/sys/unix"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetInfo retrieves and parses the system information
|
||||
func GetInfo(ctx context.Context) *Info {
|
||||
out := _getInfo()
|
||||
for strings.Contains(out, "broken pipe") {
|
||||
out = _getInfo()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
utsname := unix.Utsname{}
|
||||
err := unix.Uname(&utsname)
|
||||
if err != nil {
|
||||
fmt.Println("getInfo:", err)
|
||||
}
|
||||
osStr := strings.Replace(out, "\n", "", -1)
|
||||
osStr = strings.Replace(osStr, "\r\n", "", -1)
|
||||
osInfo := strings.Split(osStr, " ")
|
||||
gio := &Info{Kernel: osInfo[0], OSVersion: osInfo[1], Core: osInfo[1], Platform: osInfo[2], OS: osInfo[0], GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
|
||||
sysName := string(bytes.Split(utsname.Sysname[:], []byte{0})[0])
|
||||
machine := string(bytes.Split(utsname.Machine[:], []byte{0})[0])
|
||||
release := string(bytes.Split(utsname.Release[:], []byte{0})[0])
|
||||
gio := &Info{Kernel: sysName, OSVersion: release, Core: release, Platform: machine, OS: sysName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
|
||||
gio.Hostname, _ = os.Hostname()
|
||||
gio.WiretrusteeVersion = NetbirdVersion()
|
||||
gio.UIVersion = extractUserAgent(ctx)
|
||||
|
||||
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,3 +1,7 @@
|
||||
//go:build !(linux && 386)
|
||||
// +build !linux !386
|
||||
|
||||
// skipping linux 32 bits build and tests
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -5,7 +9,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -497,7 +500,7 @@ func (s *serviceClient) getSrvConfig() {
|
||||
// checkPIDFile exists and return error, or write new.
|
||||
func checkPIDFile() error {
|
||||
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 process, err := os.FindProcess(pid); err == nil {
|
||||
if err := process.Signal(syscall.Signal(0)); err == nil {
|
||||
@@ -507,5 +510,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)
|
||||
}
|
||||
|
||||
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.
|
||||
func CreateCertManager(datadir string, letsencryptDomain string) *autocert.Manager {
|
||||
func CreateCertManager(datadir string, letsencryptDomain string) (*autocert.Manager, error) {
|
||||
certDir := filepath.Join(datadir, "letsencrypt")
|
||||
|
||||
if _, err := os.Stat(certDir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(certDir, os.ModeDir)
|
||||
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{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
@@ -26,5 +26,5 @@ func CreateCertManager(datadir string, letsencryptDomain string) *autocert.Manag
|
||||
HostPolicy: autocert.HostWhitelist(letsencryptDomain),
|
||||
}
|
||||
|
||||
return certManager
|
||||
return certManager, nil
|
||||
}
|
||||
|
||||
18
go.mod
18
go.mod
@@ -17,8 +17,8 @@ require (
|
||||
github.com/spf13/cobra v1.3.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/vishvananda/netlink v1.1.0
|
||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
|
||||
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211209221555-9c9e7e272434
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20211215182854-7a385b3431de
|
||||
golang.zx2c4.com/wireguard/windows v0.5.1
|
||||
@@ -30,18 +30,23 @@ require (
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.1.4
|
||||
github.com/c-robinson/iplib v1.0.3
|
||||
github.com/creack/pty v1.1.18
|
||||
github.com/eko/gocache/v2 v2.3.1
|
||||
github.com/getlantern/systray v1.2.1
|
||||
github.com/gliderlabs/ssh v0.3.4
|
||||
github.com/magiconair/properties v1.8.5
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/rs/xid v1.3.0
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/stretchr/testify v1.7.1
|
||||
golang.org/x/net v0.0.0-20220513224357-95641704303c
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.4.1 // indirect
|
||||
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
@@ -84,23 +89,22 @@ require (
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.33.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.8.0 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
|
||||
github.com/yuin/goldmark v1.4.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf // indirect
|
||||
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect
|
||||
golang.org/x/tools v0.1.10 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
|
||||
golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
@@ -111,4 +115,4 @@ require (
|
||||
|
||||
replace github.com/pion/ice/v2 => github.com/wiretrustee/ice/v2 v2.1.21-0.20220218121004-dc81faead4bb
|
||||
|
||||
//replace github.com/eko/gocache/v3 => /home/braginini/Documents/projects/my/wiretrustee/gocache
|
||||
replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-20220901161712-56a6ec08182e
|
||||
|
||||
42
go.sum
42
go.sum
@@ -69,6 +69,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/allegro/bigcache/v3 v3.0.2 h1:AKZCw+5eAaVyNTBmI2fgyPVJhHkdWder3O9IrprcQfI=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
@@ -118,6 +120,8 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -130,8 +134,6 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/eko/gocache/v2 v2.3.1 h1:8MMkfqGJ0KIA9OXT0rXevcEIrU16oghrGDiIDJDFCa0=
|
||||
github.com/eko/gocache/v2 v2.3.1/go.mod h1:l2z8OmpZHL0CpuzDJtxm267eF3mZW1NqUsMj+sKrbUs=
|
||||
github.com/eko/gocache/v3 v3.0.0 h1:Mfa3Nj6GdrXiBXz+JsvESffxO8BGUYVQ2heiAhEhH1U=
|
||||
github.com/eko/gocache/v3 v3.0.0/go.mod h1:2//8SJUJBp0/MKvuPd6mhZuWpL3qTic4N0ssUEaflCk=
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
@@ -180,6 +182,8 @@ github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2H
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||
github.com/gliderlabs/ssh v0.3.4 h1:+AXBtim7MTKaLVPgvE+3mhewYRawNLTd+jEEz/wExZw=
|
||||
github.com/gliderlabs/ssh v0.3.4/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
|
||||
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76wBquMVETx7uveQD9MCIQoU=
|
||||
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
@@ -379,8 +383,6 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kardianos/service v1.2.1-0.20210728001519-a323c3813bc7 h1:oohm9Rk9JAxxmp2NLZa7Kebgz9h4+AJDcc64txg3dQ0=
|
||||
github.com/kardianos/service v1.2.1-0.20210728001519-a323c3813bc7/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
@@ -462,8 +464,9 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/netbirdio/service v0.0.0-20220901161712-56a6ec08182e h1:T7x1EzbvEiuvRtfNxLmbw5z2cwdXuFx+plt2lzY3nPY=
|
||||
github.com/netbirdio/service v0.0.0-20220901161712-56a6ec08182e/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
@@ -511,6 +514,7 @@ github.com/pion/turn/v2 v2.0.7 h1:SZhc00WDovK6czaN1RSiHqbwANtIO6wfZQsU0m0KNE8=
|
||||
github.com/pion/turn/v2 v2.0.7/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
||||
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
||||
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -550,7 +554,8 @@ github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so=
|
||||
github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM=
|
||||
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
|
||||
@@ -645,11 +650,13 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
|
||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c=
|
||||
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -660,8 +667,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf h1:oXVg4h2qJDd9htKxb5SCpFBHLipW6hXmL3qpUixS2jw=
|
||||
golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
|
||||
@@ -758,8 +763,8 @@ golang.org/x/net v0.0.0-20211208012354-db4efeb81f4b/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4=
|
||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220513224357-95641704303c h1:nF9mHSvoKBLkQNQhJZNsc66z2UzAMUbLGjC95CF3pU0=
|
||||
golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -891,10 +896,12 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211214234402-4825e8c3871d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -975,8 +982,9 @@ golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d h1:9+v0G0naRhLPOJEeJOL6NuXTtAHHwmkyZlgQJ0XcQ8I=
|
||||
golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d/go.mod h1:5yyfuiqVIJ7t+3MqrpTQ+QqRkMWiESiyDvPNvKYCecg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=
|
||||
@@ -1142,8 +1150,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
@@ -1201,4 +1209,4 @@ sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:w
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
|
||||
@@ -33,3 +33,8 @@ func (w *WGIface) assignAddr() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WireguardModExists check if we can load wireguard mod (linux only)
|
||||
func WireguardModExists() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type NativeLink struct {
|
||||
Link *netlink.Link
|
||||
}
|
||||
|
||||
// WireguardModExists check if we can load wireguard mod (linux only)
|
||||
func WireguardModExists() bool {
|
||||
link := newWGLink("mustnotexist")
|
||||
|
||||
|
||||
@@ -57,3 +57,8 @@ func (w *WGIface) UpdateAddr(newAddr string) error {
|
||||
w.Address = addr
|
||||
return w.assignAddr(luid)
|
||||
}
|
||||
|
||||
// WireguardModExists check if we can load wireguard mod (linux only)
|
||||
func WireguardModExists() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,30 +1,11 @@
|
||||
# Dashboard domain and auth0 configuration
|
||||
|
||||
# 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=""
|
||||
|
||||
## From this point, most settings are being done automatically, but you can edit if you need some customization
|
||||
## Most settings are being done automatically with the sourced variables from setup.env, but you can edit if you need some customization
|
||||
|
||||
# Management API
|
||||
|
||||
# Management API port
|
||||
NETBIRD_MGMT_API_PORT=33071
|
||||
# Management GRPC API port
|
||||
NETBIRD_MGMT_GRPC_API_PORT=33073
|
||||
NETBIRD_MGMT_API_PORT=33073
|
||||
# Management API endpoint address, used by the Dashboard
|
||||
NETBIRD_MGMT_API_ENDPOINT=https://$NETBIRD_DOMAIN:$NETBIRD_MGMT_API_PORT
|
||||
# Management GRPC API endpoint address, used by the hosts to register
|
||||
NETBIRD_MGMT_GRPC_API_ENDPOINT=https://$NETBIRD_DOMAIN:NETBIRD_MGMT_GRPC_API_PORT
|
||||
# Management Certficate file path. These are generated by the Dashboard container
|
||||
NETBIRD_MGMT_API_CERT_FILE="/etc/letsencrypt/live/$NETBIRD_DOMAIN/fullchain.pem"
|
||||
# Management Certficate key file path.
|
||||
@@ -46,18 +27,24 @@ MGMT_VOLUMESUFFIX="mgmt"
|
||||
SIGNAL_VOLUMESUFFIX="signal"
|
||||
LETSENCRYPT_VOLUMESUFFIX="letsencrypt"
|
||||
|
||||
NETBIRD_AUTH_DEVICE_AUTH_PROVIDER="none"
|
||||
|
||||
# exports
|
||||
export NETBIRD_DOMAIN
|
||||
export NETBIRD_AUTH0_DOMAIN
|
||||
export NETBIRD_AUTH0_CLIENT_ID
|
||||
export NETBIRD_AUTH0_AUDIENCE
|
||||
export NETBIRD_AUTH_CLIENT_ID
|
||||
export NETBIRD_AUTH_AUDIENCE
|
||||
export NETBIRD_AUTH_AUTHORITY
|
||||
export NETBIRD_USE_AUTH0
|
||||
export NETBIRD_AUTH_SUPPORTED_SCOPES
|
||||
export NETBIRD_AUTH_JWT_CERTS
|
||||
export NETBIRD_LETSENCRYPT_EMAIL
|
||||
export NETBIRD_MGMT_API_PORT
|
||||
export NETBIRD_MGMT_API_ENDPOINT
|
||||
export NETBIRD_MGMT_GRPC_API_PORT
|
||||
export NETBIRD_MGMT_GRPC_API_ENDPOINT
|
||||
export NETBIRD_MGMT_API_CERT_FILE
|
||||
export NETBIRD_MGMT_API_CERT_KEY_FILE
|
||||
export NETBIRD_AUTH_DEVICE_AUTH_PROVIDER
|
||||
export NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID
|
||||
export NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT
|
||||
export TURN_USER
|
||||
export TURN_PASSWORD
|
||||
export TURN_MIN_PORT
|
||||
@@ -1,10 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
if ! which curl > /dev/null 2>&1
|
||||
then
|
||||
echo "This script uses curl fetch OpenID configuration from IDP."
|
||||
echo "Please install curl and re-run the script https://curl.se/"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! which jq > /dev/null 2>&1
|
||||
then
|
||||
echo "This script uses jq to load OpenID configuration from IDP."
|
||||
echo "Please install jq and re-run the script https://stedolan.github.io/jq/"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source setup.env
|
||||
source base.setup.env
|
||||
|
||||
if ! which envsubst > /dev/null 2>&1
|
||||
then
|
||||
echo "envsubst is needed to run this script"
|
||||
if [[ $(uname) == "Darwin" ]]
|
||||
then
|
||||
echo "you can install it with homebrew (https://brew.sh):"
|
||||
echo "brew install gettext"
|
||||
else
|
||||
if which apt-get > /dev/null 2>&1
|
||||
then
|
||||
echo "you can install it by running"
|
||||
echo "apt-get update && apt-get install gettext-base"
|
||||
else
|
||||
echo "you can install it by installing the package gettext with your package manager"
|
||||
fi
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "x-$NETBIRD_DOMAIN" == "x-" ]]
|
||||
then
|
||||
echo NETBIRD_DOMAIN is not set, please update your setup.env file
|
||||
echo If you are migrating from old versions, you migh need to update your variables prefixes from
|
||||
echo WIRETRUSTEE_.. TO NETBIRD_
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -12,7 +50,6 @@ fi
|
||||
if [[ $NETBIRD_DOMAIN == "localhost" || $NETBIRD_DOMAIN == "127.0.0.1" ]]
|
||||
then
|
||||
export NETBIRD_MGMT_API_ENDPOINT=http://$NETBIRD_DOMAIN:$NETBIRD_MGMT_API_PORT
|
||||
export NETBIRD_MGMT_GRPC_API_ENDPOINT=http://$NETBIRD_DOMAIN:$NETBIRD_MGMT_GRPC_API_PORT
|
||||
unset NETBIRD_MGMT_API_CERT_FILE
|
||||
unset NETBIRD_MGMT_API_CERT_KEY_FILE
|
||||
fi
|
||||
@@ -42,6 +79,49 @@ export MGMT_VOLUMENAME
|
||||
export SIGNAL_VOLUMENAME
|
||||
export LETSENCRYPT_VOLUMENAME
|
||||
|
||||
#backwards compatibility after migrating to generic OIDC with Auth0
|
||||
if [[ -z "${NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT}" ]]; then
|
||||
|
||||
if [[ -z "${NETBIRD_AUTH0_DOMAIN}" ]]; then
|
||||
# not a backward compatible state
|
||||
echo "NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT property must be set in the setup.env file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "It seems like you provided an old setup.env file."
|
||||
echo "Since the release of v0.8.10, we introduced a new set of properties."
|
||||
echo "The script is backward compatible and will continue automatically."
|
||||
echo "In the future versions it will be deprecated. Please refer to the documentation to learn about the changes http://netbird.io/docs/getting-started/self-hosting"
|
||||
|
||||
export NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT="https://${NETBIRD_AUTH0_DOMAIN}/.well-known/openid-configuration"
|
||||
export NETBIRD_USE_AUTH0="true"
|
||||
export NETBIRD_AUTH_AUDIENCE=${NETBIRD_AUTH0_AUDIENCE}
|
||||
export NETBIRD_AUTH_CLIENT_ID=${NETBIRD_AUTH0_CLIENT_ID}
|
||||
fi
|
||||
|
||||
echo "loading OpenID configuration from ${NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT} to the openid-configuration.json file"
|
||||
curl "${NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT}" -q -o openid-configuration.json
|
||||
|
||||
export NETBIRD_AUTH_AUTHORITY=$( jq -r '.issuer' openid-configuration.json )
|
||||
export NETBIRD_AUTH_JWT_CERTS=$( jq -r '.jwks_uri' openid-configuration.json )
|
||||
export NETBIRD_AUTH_SUPPORTED_SCOPES=$( jq -r '.scopes_supported | join(" ")' openid-configuration.json )
|
||||
export NETBIRD_AUTH_TOKEN_ENDPOINT=$( jq -r '.token_endpoint' openid-configuration.json )
|
||||
export NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT=$( jq -r '.device_authorization_endpoint' openid-configuration.json )
|
||||
|
||||
if [ $NETBIRD_USE_AUTH0 == "true" ]
|
||||
then
|
||||
export NETBIRD_AUTH_SUPPORTED_SCOPES="openid profile email offline_access api email_verified"
|
||||
else
|
||||
export NETBIRD_AUTH_SUPPORTED_SCOPES="openid profile email offline_access api"
|
||||
fi
|
||||
|
||||
if [[ ! -z "${NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID}" ]]; then
|
||||
# user enabled Device Authorization Grant feature
|
||||
export NETBIRD_AUTH_DEVICE_AUTH_PROVIDER="hosted"
|
||||
fi
|
||||
|
||||
env | grep NETBIRD
|
||||
|
||||
envsubst < docker-compose.yml.tmpl > docker-compose.yml
|
||||
envsubst < management.json.tmpl > management.json
|
||||
envsubst < turnserver.conf.tmpl > turnserver.conf
|
||||
|
||||
@@ -8,11 +8,13 @@ services:
|
||||
- 80:80
|
||||
- 443:443
|
||||
environment:
|
||||
- AUTH0_DOMAIN=$NETBIRD_AUTH0_DOMAIN
|
||||
- AUTH0_CLIENT_ID=$NETBIRD_AUTH0_CLIENT_ID
|
||||
- AUTH0_AUDIENCE=$NETBIRD_AUTH0_AUDIENCE
|
||||
- AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
|
||||
- AUTH_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
|
||||
- AUTH_AUTHORITY=$NETBIRD_AUTH_AUTHORITY
|
||||
- USE_AUTH0=$NETBIRD_USE_AUTH0
|
||||
- AUTH_SUPPORTED_SCOPES=$NETBIRD_AUTH_SUPPORTED_SCOPES
|
||||
- NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
||||
- NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_MGMT_GRPC_API_ENDPOINT
|
||||
- NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
||||
- NGINX_SSL_PORT=443
|
||||
- LETSENCRYPT_DOMAIN=$NETBIRD_DOMAIN
|
||||
- LETSENCRYPT_EMAIL=$NETBIRD_LETSENCRYPT_EMAIL
|
||||
@@ -25,7 +27,7 @@ services:
|
||||
volumes:
|
||||
- $SIGNAL_VOLUMENAME:/var/lib/netbird
|
||||
ports:
|
||||
- 10000:10000
|
||||
- 10000:80
|
||||
# # port and command for Let's Encrypt validation
|
||||
# - 443:443
|
||||
# command: ["--letsencrypt-domain", "$NETBIRD_DOMAIN", "--log-file", "console"]
|
||||
@@ -40,11 +42,11 @@ services:
|
||||
- $LETSENCRYPT_VOLUMENAME:/etc/letsencrypt:ro
|
||||
- ./management.json:/etc/netbird/management.json
|
||||
ports:
|
||||
- $NETBIRD_MGMT_GRPC_API_PORT:33073 #gRPC port
|
||||
- $NETBIRD_MGMT_API_PORT:33071 #API port
|
||||
# # port and command for Let's Encrypt validation
|
||||
# - 443:443
|
||||
- $NETBIRD_MGMT_API_PORT:443 #API port
|
||||
# # port and command for Let's Encrypt validation without dashboard container
|
||||
# - 443:443
|
||||
# command: ["--letsencrypt-domain", "$NETBIRD_DOMAIN", "--log-file", "console"]
|
||||
command: ["--port", "443", "--log-file", "console"]
|
||||
# Coturn
|
||||
coturn:
|
||||
image: coturn/coturn
|
||||
@@ -58,4 +60,4 @@ services:
|
||||
volumes:
|
||||
$MGMT_VOLUMENAME:
|
||||
$SIGNAL_VOLUMENAME:
|
||||
$LETSENCRYPT_VOLUMENAME:
|
||||
$LETSENCRYPT_VOLUMENAME:
|
||||
|
||||
@@ -29,13 +29,24 @@
|
||||
"Datadir": "",
|
||||
"HttpConfig": {
|
||||
"Address": "0.0.0.0:$NETBIRD_MGMT_API_PORT",
|
||||
"AuthIssuer": "https://$NETBIRD_AUTH0_DOMAIN/",
|
||||
"AuthAudience": "$NETBIRD_AUTH0_AUDIENCE",
|
||||
"AuthKeysLocation": "https://$NETBIRD_AUTH0_DOMAIN/.well-known/jwks.json",
|
||||
"AuthIssuer": "$NETBIRD_AUTH_AUTHORITY",
|
||||
"AuthAudience": "$NETBIRD_AUTH_AUDIENCE",
|
||||
"AuthKeysLocation": "$NETBIRD_AUTH_JWT_CERTS",
|
||||
"CertFile":"$NETBIRD_MGMT_API_CERT_FILE",
|
||||
"CertKey":"$NETBIRD_MGMT_API_CERT_KEY_FILE"
|
||||
"CertKey":"$NETBIRD_MGMT_API_CERT_KEY_FILE",
|
||||
"OIDCConfigEndpoint":"$NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT"
|
||||
},
|
||||
"IdpManagerConfig": {
|
||||
"Manager": "none"
|
||||
}
|
||||
},
|
||||
"DeviceAuthorizationFlow": {
|
||||
"Provider": "$NETBIRD_AUTH_DEVICE_AUTH_PROVIDER",
|
||||
"ProviderConfig": {
|
||||
"Audience": "$NETBIRD_AUTH_AUDIENCE",
|
||||
"Domain": "$NETBIRD_AUTH0_DOMAIN",
|
||||
"ClientID": "$NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID",
|
||||
"TokenEndpoint": "$NETBIRD_AUTH_TOKEN_ENDPOINT",
|
||||
"DeviceAuthEndpoint": "$NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
infrastructure_files/setup.env.example
Normal file
15
infrastructure_files/setup.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
## example file, you can copy this file to setup.env and update its values
|
||||
##
|
||||
# Dashboard domain. e.g. app.mydomain.com
|
||||
NETBIRD_DOMAIN=""
|
||||
# OIDC configuration e.g., https://example.eu.auth0.com/.well-known/openid-configuration
|
||||
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT=""
|
||||
NETBIRD_AUTH_AUDIENCE=""
|
||||
# e.g. netbird-client
|
||||
NETBIRD_AUTH_CLIENT_ID=""
|
||||
# indicates whether to use Auth0 or not: true or false
|
||||
NETBIRD_USE_AUTH0="false"
|
||||
NETBIRD_AUTH_DEVICE_AUTH_PROVIDER="none"
|
||||
NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID=""
|
||||
# e.g. hello@mydomain.com
|
||||
NETBIRD_LETSENCRYPT_EMAIL=""
|
||||
13
infrastructure_files/tests/setup.env
Normal file
13
infrastructure_files/tests/setup.env
Normal file
@@ -0,0 +1,13 @@
|
||||
## example file, you can copy this file to setup.env and update its values
|
||||
##
|
||||
# Dashboard domain. e.g. app.mydomain.com
|
||||
NETBIRD_DOMAIN="localhost"
|
||||
# e.g. https://dev-24vkclam.us.auth0.com/ or https://YOUR-KEYCLOAK-HOST:8080/realms/netbird
|
||||
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT="https://example.eu.auth0.com/.well-known/openid-configuration"
|
||||
# e.g. netbird-client
|
||||
NETBIRD_AUTH_CLIENT_ID=$CI_NETBIRD_AUTH_CLIENT_ID
|
||||
# indicates whether to use Auth0 or not: true or false
|
||||
NETBIRD_USE_AUTH0=$CI_NETBIRD_USE_AUTH0
|
||||
NETBIRD_AUTH_AUDIENCE=$CI_NETBIRD_AUTH_AUDIENCE
|
||||
# e.g. hello@mydomain.com
|
||||
NETBIRD_LETSENCRYPT_EMAIL=""
|
||||
@@ -12,7 +12,7 @@ type Client interface {
|
||||
io.Closer
|
||||
Sync(msgHandler func(msg *proto.SyncResponse) error) error
|
||||
GetServerPublicKey() (*wgtypes.Key, error)
|
||||
Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info) (*proto.LoginResponse, error)
|
||||
Login(serverKey wgtypes.Key, sysInfo *system.Info) (*proto.LoginResponse, error)
|
||||
Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error)
|
||||
Login(serverKey wgtypes.Key, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error)
|
||||
GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error)
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ func TestClient_LoginUnregistered_ShouldThrow_401(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sysInfo := system.GetInfo(context.TODO())
|
||||
_, err = client.Login(*key, sysInfo)
|
||||
_, err = client.Login(*key, sysInfo, nil)
|
||||
if err == nil {
|
||||
t.Error("expecting err on unregistered login, got nil")
|
||||
}
|
||||
@@ -186,7 +186,7 @@ func TestClient_LoginRegistered(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
info := system.GetInfo(context.TODO())
|
||||
resp, err := client.Register(*key, ValidKey, "", info)
|
||||
resp, err := client.Register(*key, ValidKey, "", info, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -216,7 +216,7 @@ func TestClient_Sync(t *testing.T) {
|
||||
}
|
||||
|
||||
info := system.GetInfo(context.TODO())
|
||||
_, err = client.Register(*serverKey, ValidKey, "", info)
|
||||
_, err = client.Register(*serverKey, ValidKey, "", info, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -232,7 +232,7 @@ func TestClient_Sync(t *testing.T) {
|
||||
}
|
||||
|
||||
info = system.GetInfo(context.TODO())
|
||||
_, err = remoteClient.Register(*serverKey, ValidKey, "", info)
|
||||
_, err = remoteClient.Register(*serverKey, ValidKey, "", info, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -330,7 +330,7 @@ func Test_SystemMetaDataFromClient(t *testing.T) {
|
||||
}
|
||||
|
||||
info := system.GetInfo(context.TODO())
|
||||
_, err = testClient.Register(*key, ValidKey, "", info)
|
||||
_, err = testClient.Register(*key, ValidKey, "", info, nil)
|
||||
if err != nil {
|
||||
t.Errorf("error while trying to register client: %v", err)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func NewClient(ctx context.Context, addr string, ourPrivateKey wgtypes.Key, tlsE
|
||||
transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))
|
||||
}
|
||||
|
||||
mgmCtx, cancel := context.WithTimeout(ctx, time.Second*3)
|
||||
mgmCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
conn, err := grpc.DialContext(
|
||||
mgmCtx,
|
||||
@@ -72,10 +72,10 @@ func (c *GrpcClient) Close() error {
|
||||
func defaultBackoff(ctx context.Context) backoff.BackOff {
|
||||
return backoff.WithContext(&backoff.ExponentialBackOff{
|
||||
InitialInterval: 800 * time.Millisecond,
|
||||
RandomizationFactor: backoff.DefaultRandomizationFactor,
|
||||
Multiplier: backoff.DefaultMultiplier,
|
||||
RandomizationFactor: 1,
|
||||
Multiplier: 1.7,
|
||||
MaxInterval: 10 * time.Second,
|
||||
MaxElapsedTime: 12 * time.Hour, // stop after 12 hours of trying, the error will be propagated to the general retry of the client
|
||||
MaxElapsedTime: 3 * 30 * 24 * time.Hour, // 3 months
|
||||
Stop: backoff.Stop,
|
||||
Clock: backoff.SystemClock,
|
||||
}, ctx)
|
||||
@@ -95,20 +95,26 @@ func (c *GrpcClient) Sync(msgHandler func(msg *proto.SyncResponse) error) error
|
||||
operation := func() error {
|
||||
log.Debugf("management connection state %v", c.conn.GetState())
|
||||
|
||||
if !c.ready() {
|
||||
return fmt.Errorf("no connection to management")
|
||||
connState := c.conn.GetState()
|
||||
if connState == connectivity.Shutdown {
|
||||
return backoff.Permanent(fmt.Errorf("connection to management has been shut down"))
|
||||
} else if !(connState == connectivity.Ready || connState == connectivity.Idle) {
|
||||
c.conn.WaitForStateChange(c.ctx, connState)
|
||||
return fmt.Errorf("connection to management is not ready and in %s state", connState)
|
||||
}
|
||||
|
||||
// todo we already have it since we did the Login, maybe cache it locally?
|
||||
serverPubKey, err := c.GetServerPublicKey()
|
||||
if err != nil {
|
||||
log.Errorf("failed getting Management Service public key: %s", err)
|
||||
log.Debugf("failed getting Management Service public key: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
stream, err := c.connectToStream(*serverPubKey)
|
||||
if err != nil {
|
||||
log.Errorf("failed to open Management Service stream: %s", err)
|
||||
log.Debugf("failed to open Management Service stream: %s", err)
|
||||
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
||||
return backoff.Permanent(err) // unrecoverable error, propagate to the upper layer
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -117,10 +123,13 @@ func (c *GrpcClient) Sync(msgHandler func(msg *proto.SyncResponse) error) error
|
||||
// blocking until error
|
||||
err = c.receiveEvents(stream, *serverPubKey, msgHandler)
|
||||
if err != nil {
|
||||
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
|
||||
return backoff.Permanent(err)
|
||||
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
||||
return backoff.Permanent(err) // unrecoverable error, propagate to the upper layer
|
||||
}
|
||||
// we need this reset because after a successful connection and a consequent error, backoff lib doesn't
|
||||
// reset times and next try will start with a long delay
|
||||
backOff.Reset()
|
||||
log.Warnf("disconnected from the Management service but will retry silently. Reason: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -129,7 +138,7 @@ func (c *GrpcClient) Sync(msgHandler func(msg *proto.SyncResponse) error) error
|
||||
|
||||
err := backoff.Retry(operation, backOff)
|
||||
if err != nil {
|
||||
log.Warnf("exiting Management Service connection retry loop due to Permanent error: %s", err)
|
||||
log.Warnf("exiting the Management service connection retry loop due to the unrecoverable error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -156,11 +165,11 @@ func (c *GrpcClient) receiveEvents(stream proto.ManagementService_SyncClient, se
|
||||
for {
|
||||
update, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
log.Errorf("Management stream has been closed by server: %s", err)
|
||||
log.Debugf("Management stream has been closed by server: %s", err)
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
log.Warnf("disconnected from Management Service sync stream: %v", err)
|
||||
log.Debugf("disconnected from Management Service sync stream: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -180,13 +189,13 @@ func (c *GrpcClient) receiveEvents(stream proto.ManagementService_SyncClient, se
|
||||
}
|
||||
}
|
||||
|
||||
// GetServerPublicKey returns server Wireguard public key (used later for encrypting messages sent to the server)
|
||||
// GetServerPublicKey returns server's WireGuard public key (used later for encrypting messages sent to the server)
|
||||
func (c *GrpcClient) GetServerPublicKey() (*wgtypes.Key, error) {
|
||||
if !c.ready() {
|
||||
return nil, fmt.Errorf("no connection to management")
|
||||
}
|
||||
|
||||
mgmCtx, cancel := context.WithTimeout(c.ctx, time.Second*2)
|
||||
mgmCtx, cancel := context.WithTimeout(c.ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
resp, err := c.realClient.GetServerKey(mgmCtx, &proto.Empty{})
|
||||
if err != nil {
|
||||
@@ -210,7 +219,7 @@ func (c *GrpcClient) login(serverKey wgtypes.Key, req *proto.LoginRequest) (*pro
|
||||
log.Errorf("failed to encrypt message: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
mgmCtx, cancel := context.WithTimeout(c.ctx, time.Second*2)
|
||||
mgmCtx, cancel := context.WithTimeout(c.ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
resp, err := c.realClient.Login(mgmCtx, &proto.EncryptedMessage{
|
||||
WgPubKey: c.key.PublicKey().String(),
|
||||
@@ -233,13 +242,21 @@ func (c *GrpcClient) login(serverKey wgtypes.Key, req *proto.LoginRequest) (*pro
|
||||
// Register registers peer on Management Server. It actually calls a Login endpoint with a provided setup key
|
||||
// Takes care of encrypting and decrypting messages.
|
||||
// This method will also collect system info and send it with the request (e.g. hostname, os, etc)
|
||||
func (c *GrpcClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info) (*proto.LoginResponse, error) {
|
||||
return c.login(serverKey, &proto.LoginRequest{SetupKey: setupKey, Meta: infoToMetaData(sysInfo), JwtToken: jwtToken})
|
||||
func (c *GrpcClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, pubSSHKey []byte) (*proto.LoginResponse, error) {
|
||||
keys := &proto.PeerKeys{
|
||||
SshPubKey: pubSSHKey,
|
||||
WgPubKey: []byte(c.key.PublicKey().String()),
|
||||
}
|
||||
return c.login(serverKey, &proto.LoginRequest{SetupKey: setupKey, Meta: infoToMetaData(sysInfo), JwtToken: jwtToken, PeerKeys: keys})
|
||||
}
|
||||
|
||||
// Login attempts login to Management Server. Takes care of encrypting and decrypting messages.
|
||||
func (c *GrpcClient) Login(serverKey wgtypes.Key, sysInfo *system.Info) (*proto.LoginResponse, error) {
|
||||
return c.login(serverKey, &proto.LoginRequest{Meta: infoToMetaData(sysInfo)})
|
||||
func (c *GrpcClient) Login(serverKey wgtypes.Key, sysInfo *system.Info, pubSSHKey []byte) (*proto.LoginResponse, error) {
|
||||
keys := &proto.PeerKeys{
|
||||
SshPubKey: pubSSHKey,
|
||||
WgPubKey: []byte(c.key.PublicKey().String()),
|
||||
}
|
||||
return c.login(serverKey, &proto.LoginRequest{Meta: infoToMetaData(sysInfo), PeerKeys: keys})
|
||||
}
|
||||
|
||||
// GetDeviceAuthorizationFlow returns a device authorization flow information.
|
||||
|
||||
@@ -10,8 +10,8 @@ type MockClient struct {
|
||||
CloseFunc func() error
|
||||
SyncFunc func(msgHandler func(msg *proto.SyncResponse) error) error
|
||||
GetServerPublicKeyFunc func() (*wgtypes.Key, error)
|
||||
RegisterFunc func(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info) (*proto.LoginResponse, error)
|
||||
LoginFunc func(serverKey wgtypes.Key, info *system.Info) (*proto.LoginResponse, error)
|
||||
RegisterFunc func(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte) (*proto.LoginResponse, error)
|
||||
LoginFunc func(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*proto.LoginResponse, error)
|
||||
GetDeviceAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error)
|
||||
}
|
||||
|
||||
@@ -36,18 +36,18 @@ func (m *MockClient) GetServerPublicKey() (*wgtypes.Key, error) {
|
||||
return m.GetServerPublicKeyFunc()
|
||||
}
|
||||
|
||||
func (m *MockClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info) (*proto.LoginResponse, error) {
|
||||
func (m *MockClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) {
|
||||
if m.RegisterFunc == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return m.RegisterFunc(serverKey, setupKey, jwtToken, info)
|
||||
return m.RegisterFunc(serverKey, setupKey, jwtToken, info, sshKey)
|
||||
}
|
||||
|
||||
func (m *MockClient) Login(serverKey wgtypes.Key, info *system.Info) (*proto.LoginResponse, error) {
|
||||
func (m *MockClient) Login(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) {
|
||||
if m.LoginFunc == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return m.LoginFunc(serverKey, info)
|
||||
return m.LoginFunc(serverKey, info, sshKey)
|
||||
}
|
||||
|
||||
func (m *MockClient) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) {
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
httpapi "github.com/netbirdio/netbird/management/server/http"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
|
||||
@@ -28,11 +33,16 @@ import (
|
||||
"google.golang.org/grpc/keepalive"
|
||||
)
|
||||
|
||||
// ManagementLegacyPort is the port that was used before by the Management gRPC server.
|
||||
// It is used for backward compatibility now.
|
||||
const ManagementLegacyPort = 33073
|
||||
|
||||
var (
|
||||
mgmtPort int
|
||||
mgmtLetsencryptDomain string
|
||||
certFile string
|
||||
certKey string
|
||||
config *server.Config
|
||||
|
||||
kaep = keepalive.EnforcementPolicy{
|
||||
MinTime: 15 * time.Second,
|
||||
@@ -48,34 +58,55 @@ var (
|
||||
|
||||
mgmtCmd = &cobra.Command{
|
||||
Use: "management",
|
||||
Short: "start Netbird Management Server",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Short: "start NetBird Management Server",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// detect whether user specified a port
|
||||
userPort := cmd.Flag("port").Changed
|
||||
|
||||
var err error
|
||||
config, err = loadMgmtConfig(mgmtConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed reading provided config file: %s: %v", mgmtConfig, err)
|
||||
}
|
||||
|
||||
tlsEnabled := false
|
||||
if mgmtLetsencryptDomain != "" || (config.HttpConfig.CertFile != "" && config.HttpConfig.CertKey != "") {
|
||||
tlsEnabled = true
|
||||
}
|
||||
|
||||
if !userPort {
|
||||
// different defaults for port when tls enabled/disabled
|
||||
if tlsEnabled {
|
||||
mgmtPort = 443
|
||||
} else {
|
||||
mgmtPort = 80
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
flag.Parse()
|
||||
err := util.InitLog(logLevel, logFile)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing log %v", err)
|
||||
return fmt.Errorf("failed initializing log %v", err)
|
||||
}
|
||||
|
||||
err = handleRebrand(cmd)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to migrate files %v", err)
|
||||
}
|
||||
|
||||
config, err := loadMgmtConfig(mgmtConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("failed reading provided config file: %s: %v", mgmtConfig, err)
|
||||
return fmt.Errorf("failed to migrate files %v", err)
|
||||
}
|
||||
|
||||
if _, err = os.Stat(config.Datadir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(config.Datadir, os.ModeDir)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating datadir: %s: %v", config.Datadir, err)
|
||||
return fmt.Errorf("failed creating datadir: %s: %v", config.Datadir, err)
|
||||
}
|
||||
}
|
||||
|
||||
store, err := server.NewStore(config.Datadir)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
|
||||
return fmt.Errorf("failed creating Store: %s: %v", config.Datadir, err)
|
||||
}
|
||||
peersUpdateManager := server.NewPeersUpdateManager()
|
||||
|
||||
@@ -83,82 +114,180 @@ var (
|
||||
if config.IdpManagerConfig != nil {
|
||||
idpManager, err = idp.NewManager(*config.IdpManagerConfig)
|
||||
if err != nil {
|
||||
log.Fatalln("failed retrieving a new idp manager with err: ", err)
|
||||
return fmt.Errorf("failed retrieving a new idp manager with err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
accountManager, err := server.BuildManager(store, peersUpdateManager, idpManager)
|
||||
if err != nil {
|
||||
log.Fatalln("failed build default manager: ", err)
|
||||
return fmt.Errorf("failed to build default manager: %v", err)
|
||||
}
|
||||
|
||||
var opts []grpc.ServerOption
|
||||
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
|
||||
|
||||
var httpServer *http.Server
|
||||
gRPCOpts := []grpc.ServerOption{grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp)}
|
||||
var certManager *autocert.Manager
|
||||
var tlsConfig *tls.Config
|
||||
tlsEnabled := false
|
||||
if config.HttpConfig.LetsEncryptDomain != "" {
|
||||
// automatically generate a new certificate with Let's Encrypt
|
||||
certManager := encryption.CreateCertManager(config.Datadir, config.HttpConfig.LetsEncryptDomain)
|
||||
transportCredentials := credentials.NewTLS(certManager.TLSConfig())
|
||||
opts = append(opts, grpc.Creds(transportCredentials))
|
||||
|
||||
httpServer = http.NewHttpsServer(config.HttpConfig, certManager, accountManager)
|
||||
} else if config.HttpConfig.CertFile != "" && config.HttpConfig.CertKey != "" {
|
||||
// use provided certificate
|
||||
tlsConfig, err := loadTLSConfig(config.HttpConfig.CertFile, config.HttpConfig.CertKey)
|
||||
certManager, err = encryption.CreateCertManager(config.Datadir, config.HttpConfig.LetsEncryptDomain)
|
||||
if err != nil {
|
||||
log.Fatal("cannot load TLS credentials: ", err)
|
||||
return fmt.Errorf("failed creating LetsEncrypt cert manager: %v", err)
|
||||
}
|
||||
transportCredentials := credentials.NewTLS(certManager.TLSConfig())
|
||||
gRPCOpts = append(gRPCOpts, grpc.Creds(transportCredentials))
|
||||
tlsEnabled = true
|
||||
} else if config.HttpConfig.CertFile != "" && config.HttpConfig.CertKey != "" {
|
||||
tlsConfig, err = loadTLSConfig(config.HttpConfig.CertFile, config.HttpConfig.CertKey)
|
||||
if err != nil {
|
||||
log.Errorf("cannot load TLS credentials: %v", err)
|
||||
return err
|
||||
}
|
||||
transportCredentials := credentials.NewTLS(tlsConfig)
|
||||
opts = append(opts, grpc.Creds(transportCredentials))
|
||||
httpServer = http.NewHttpsServerWithTLSConfig(config.HttpConfig, tlsConfig, accountManager)
|
||||
} else {
|
||||
// start server without SSL
|
||||
httpServer = http.NewHttpServer(config.HttpConfig, accountManager)
|
||||
gRPCOpts = append(gRPCOpts, grpc.Creds(transportCredentials))
|
||||
tlsEnabled = true
|
||||
}
|
||||
|
||||
opts = append(opts, grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
||||
grpcServer := grpc.NewServer(opts...)
|
||||
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
|
||||
server, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
|
||||
httpAPIHandler, err := httpapi.APIHandler(accountManager,
|
||||
config.HttpConfig.AuthIssuer, config.HttpConfig.AuthAudience, config.HttpConfig.AuthKeysLocation)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating new server: %v", err)
|
||||
return fmt.Errorf("failed creating HTTP API handler: %v", err)
|
||||
}
|
||||
mgmtProto.RegisterManagementServiceServer(grpcServer, server)
|
||||
log.Printf("started server: localhost:%v", mgmtPort)
|
||||
|
||||
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", mgmtPort))
|
||||
gRPCAPIHandler := grpc.NewServer(gRPCOpts...)
|
||||
srv, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to listen: %v", err)
|
||||
return fmt.Errorf("failed creating gRPC API handler: %v", err)
|
||||
}
|
||||
mgmtProto.RegisterManagementServiceServer(gRPCAPIHandler, srv)
|
||||
|
||||
go func() {
|
||||
if err = grpcServer.Serve(lis); err != nil {
|
||||
log.Fatalf("failed to serve gRpc server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
err = httpServer.Start()
|
||||
var compatListener net.Listener
|
||||
if mgmtPort != ManagementLegacyPort {
|
||||
// The Management gRPC server was running on port 33073 previously. Old agents that are already connected to it
|
||||
// are using port 33073. For compatibility purposes we keep running a 2nd gRPC server on port 33073.
|
||||
compatListener, err = serveGRPC(gRPCAPIHandler, ManagementLegacyPort)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to serve http server: %v", err)
|
||||
return err
|
||||
}
|
||||
}()
|
||||
log.Infof("running gRPC backward compatibility server: %s", compatListener.Addr().String())
|
||||
}
|
||||
|
||||
rootHandler := handlerFunc(gRPCAPIHandler, httpAPIHandler)
|
||||
var listener net.Listener
|
||||
if certManager != nil {
|
||||
// a call to certManager.Listener() always creates a new listener so we do it once
|
||||
cml := certManager.Listener()
|
||||
if mgmtPort == 443 {
|
||||
// CertManager, HTTP and gRPC API all on the same port
|
||||
rootHandler = certManager.HTTPHandler(rootHandler)
|
||||
listener = cml
|
||||
} else {
|
||||
listener, err = tls.Listen("tcp", fmt.Sprintf(":%d", mgmtPort), certManager.TLSConfig())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed creating TLS listener on port %d: %v", mgmtPort, err)
|
||||
}
|
||||
log.Infof("running HTTP server (LetsEncrypt challenge handler): %s", cml.Addr().String())
|
||||
serveHTTP(cml, certManager.HTTPHandler(nil))
|
||||
}
|
||||
} else if tlsConfig != nil {
|
||||
listener, err = tls.Listen("tcp", fmt.Sprintf(":%d", mgmtPort), tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed creating TLS listener on port %d: %v", mgmtPort, err)
|
||||
}
|
||||
} else {
|
||||
listener, err = net.Listen("tcp", fmt.Sprintf(":%d", mgmtPort))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed creating TCP listener on port %d: %v", mgmtPort, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("running HTTP server and gRPC server on the same port: %s", listener.Addr().String())
|
||||
serveGRPCWithHTTP(listener, rootHandler, tlsEnabled)
|
||||
|
||||
SetupCloseHandler()
|
||||
<-stopCh
|
||||
log.Println("Receive signal to stop running Management server")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
err = httpServer.Stop(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("failed stopping the http server %v", err)
|
||||
}
|
||||
|
||||
grpcServer.Stop()
|
||||
<-stopCh
|
||||
_ = listener.Close()
|
||||
if certManager != nil {
|
||||
_ = certManager.Listener().Close()
|
||||
}
|
||||
gRPCAPIHandler.Stop()
|
||||
log.Infof("stopped Management Service")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func notifyStop(msg string) {
|
||||
select {
|
||||
case stopCh <- 1:
|
||||
log.Error(msg)
|
||||
default:
|
||||
// stop has been already called, nothing to report
|
||||
}
|
||||
}
|
||||
|
||||
func serveGRPC(grpcServer *grpc.Server, port int) (net.Listener, error) {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
err := grpcServer.Serve(listener)
|
||||
if err != nil {
|
||||
notifyStop(fmt.Sprintf("failed running gRPC server on port %d: %v", port, err))
|
||||
}
|
||||
}()
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func serveHTTP(httpListener net.Listener, handler http.Handler) {
|
||||
go func() {
|
||||
err := http.Serve(httpListener, handler)
|
||||
if err != nil {
|
||||
notifyStop(fmt.Sprintf("failed running HTTP server: %v", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func serveGRPCWithHTTP(listener net.Listener, handler http.Handler, tlsEnabled bool) {
|
||||
go func() {
|
||||
var err error
|
||||
if tlsEnabled {
|
||||
err = http.Serve(listener, handler)
|
||||
} else {
|
||||
// the following magic is needed to support HTTP2 without TLS
|
||||
// and still share a single port between gRPC and HTTP APIs
|
||||
h1s := &http.Server{
|
||||
Handler: h2c.NewHandler(handler, &http2.Server{}),
|
||||
}
|
||||
err = h1s.Serve(listener)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
select {
|
||||
case stopCh <- 1:
|
||||
log.Errorf("failed to serve HTTP and gRPC server: %v", err)
|
||||
default:
|
||||
// stop has been already called, nothing to report
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func handlerFunc(gRPCHandler *grpc.Server, httpHandler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
grpcHeader := strings.HasPrefix(request.Header.Get("Content-Type"), "application/grpc") ||
|
||||
strings.HasPrefix(request.Header.Get("Content-Type"), "application/grpc+proto")
|
||||
if request.ProtoMajor == 2 && grpcHeader {
|
||||
gRPCHandler.ServeHTTP(writer, request)
|
||||
} else {
|
||||
httpHandler.ServeHTTP(writer, request)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
|
||||
config := &server.Config{}
|
||||
_, err := util.ReadJson(mgmtConfigPath, config)
|
||||
@@ -177,9 +306,88 @@ func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
|
||||
config.HttpConfig.CertKey = certKey
|
||||
}
|
||||
|
||||
oidcEndpoint := config.HttpConfig.OIDCConfigEndpoint
|
||||
if oidcEndpoint != "" {
|
||||
// if OIDCConfigEndpoint is specified, we can load DeviceAuthEndpoint and TokenEndpoint automatically
|
||||
log.Infof("loading OIDC configuration from the provided IDP configuration endpoint %s", oidcEndpoint)
|
||||
oidcConfig, err := fetchOIDCConfig(oidcEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("loaded OIDC configuration from the provided IDP configuration endpoint: %s", oidcEndpoint)
|
||||
|
||||
log.Infof("overriding HttpConfig.AuthIssuer with a new value %s, previously configured value: %s",
|
||||
oidcConfig.Issuer, config.HttpConfig.AuthIssuer)
|
||||
config.HttpConfig.AuthIssuer = oidcConfig.Issuer
|
||||
|
||||
log.Infof("overriding HttpConfig.AuthKeysLocation (JWT certs) with a new value %s, previously configured value: %s",
|
||||
oidcConfig.JwksURI, config.HttpConfig.AuthKeysLocation)
|
||||
config.HttpConfig.AuthKeysLocation = oidcConfig.JwksURI
|
||||
|
||||
if !(config.DeviceAuthorizationFlow == nil || strings.ToLower(config.DeviceAuthorizationFlow.Provider) == string(server.NONE)) {
|
||||
log.Infof("overriding DeviceAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
|
||||
oidcConfig.TokenEndpoint, config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint)
|
||||
config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
|
||||
log.Infof("overriding DeviceAuthorizationFlow.DeviceAuthEndpoint with a new value: %s, previously configured value: %s",
|
||||
oidcConfig.DeviceAuthEndpoint, config.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint)
|
||||
config.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint = oidcConfig.DeviceAuthEndpoint
|
||||
|
||||
u, err := url.Parse(oidcEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("overriding DeviceAuthorizationFlow.ProviderConfig.Domain with a new value: %s, previously configured value: %s",
|
||||
u.Host, config.DeviceAuthorizationFlow.ProviderConfig.Domain)
|
||||
config.DeviceAuthorizationFlow.ProviderConfig.Domain = u.Host
|
||||
}
|
||||
}
|
||||
|
||||
return config, err
|
||||
}
|
||||
|
||||
// OIDCConfigResponse used for parsing OIDC config response
|
||||
type OIDCConfigResponse struct {
|
||||
Issuer string `json:"issuer"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
DeviceAuthEndpoint string `json:"device_authorization_endpoint"`
|
||||
JwksURI string `json:"jwks_uri"`
|
||||
}
|
||||
|
||||
// fetchOIDCConfig fetches OIDC configuration from the IDP
|
||||
func fetchOIDCConfig(oidcEndpoint string) (OIDCConfigResponse, error) {
|
||||
|
||||
res, err := http.Get(oidcEndpoint)
|
||||
if err != nil {
|
||||
return OIDCConfigResponse{}, fmt.Errorf("failed fetching OIDC configuration fro mendpoint %s %v", oidcEndpoint, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := res.Body.Close()
|
||||
if err != nil {
|
||||
log.Debugf("failed closing response body %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return OIDCConfigResponse{}, fmt.Errorf("failed reading OIDC configuration response body: %v", err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return OIDCConfigResponse{}, fmt.Errorf("OIDC configuration request returned status %d with response: %s",
|
||||
res.StatusCode, string(body))
|
||||
}
|
||||
|
||||
config := OIDCConfigResponse{}
|
||||
err = json.Unmarshal(body, &config)
|
||||
if err != nil {
|
||||
return OIDCConfigResponse{}, fmt.Errorf("failed unmarshaling OIDC configuration response: %v", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
|
||||
}
|
||||
|
||||
func loadTLSConfig(certFile string, certKey string) (*tls.Config, error) {
|
||||
// Load server's certificate and private key
|
||||
serverCert, err := tls.LoadX509KeyPair(certFile, certKey)
|
||||
@@ -191,6 +399,9 @@ func loadTLSConfig(certFile string, certKey string) (*tls.Config, error) {
|
||||
config := &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
ClientAuth: tls.NoClientCert,
|
||||
NextProtos: []string{
|
||||
"h2", "http/1.1", // enable HTTP/2
|
||||
},
|
||||
}
|
||||
|
||||
return config, nil
|
||||
@@ -263,7 +474,7 @@ func copySymLink(source, dest string) error {
|
||||
|
||||
func cpDir(src string, dst string) error {
|
||||
var err error
|
||||
var fds []os.FileInfo
|
||||
var fds []os.DirEntry
|
||||
var srcinfo os.FileInfo
|
||||
|
||||
if srcinfo, err = os.Stat(src); err != nil {
|
||||
@@ -274,7 +485,7 @@ func cpDir(src string, dst string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if fds, err = ioutil.ReadDir(src); err != nil {
|
||||
if fds, err = os.ReadDir(src); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, fd := range fds {
|
||||
|
||||
@@ -60,7 +60,7 @@ func init() {
|
||||
oldDefaultMgmtConfig = oldDefaultMgmtConfigDir + "/management.json"
|
||||
oldDefaultLogFile = oldDefaultLogDir + "/management.log"
|
||||
|
||||
mgmtCmd.Flags().IntVar(&mgmtPort, "port", 33073, "server port to listen on")
|
||||
mgmtCmd.Flags().IntVar(&mgmtPort, "port", 80, "server port to listen on (defaults to 443 if TLS is enabled, 80 otherwise")
|
||||
mgmtCmd.Flags().StringVar(&mgmtDataDir, "datadir", defaultMgmtDataDir, "server data directory location")
|
||||
mgmtCmd.Flags().StringVar(&mgmtConfig, "config", defaultMgmtConfig, "Netbird config file location. Config params specified via command line (e.g. datadir) have a precedence over configuration from this file")
|
||||
mgmtCmd.Flags().StringVar(&mgmtLetsencryptDomain, "letsencrypt-domain", "", "a domain to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -71,9 +71,21 @@ message LoginRequest {
|
||||
PeerSystemMeta meta = 2;
|
||||
// SSO token (can be empty)
|
||||
string jwtToken = 3;
|
||||
// Can be absent for now.
|
||||
PeerKeys peerKeys = 4;
|
||||
|
||||
}
|
||||
// PeerKeys is additional peer info like SSH pub key and WireGuard public key.
|
||||
// This message is sent on Login or register requests, or when a key rotation has to happen.
|
||||
message PeerKeys {
|
||||
|
||||
// sshPubKey represents a public SSH key of the peer. Can be absent.
|
||||
bytes sshPubKey = 1;
|
||||
// wgPubKey represents a public WireGuard key of the peer. Can be absent.
|
||||
bytes wgPubKey = 2;
|
||||
}
|
||||
|
||||
// Peer machine meta data
|
||||
// PeerSystemMeta is machine meta data like OS and version.
|
||||
message PeerSystemMeta {
|
||||
string hostname = 1;
|
||||
string goOS = 2;
|
||||
@@ -143,6 +155,9 @@ message PeerConfig {
|
||||
string address = 1;
|
||||
// Wiretrustee DNS server (a Wireguard DNS config)
|
||||
string dns = 2;
|
||||
|
||||
// SSHConfig of the peer.
|
||||
SSHConfig sshConfig = 3;
|
||||
}
|
||||
|
||||
// NetworkMap represents a network state of the peer with the corresponding configuration parameters to establish peer-to-peer connections
|
||||
@@ -161,6 +176,8 @@ message NetworkMap {
|
||||
// Indicates whether remotePeers array is empty or not to bypass protobuf null and empty array equality.
|
||||
bool remotePeersIsEmpty = 4;
|
||||
|
||||
// List of routes to be applied
|
||||
repeated Route Routes = 5;
|
||||
}
|
||||
|
||||
// RemotePeerConfig represents a configuration of a remote peer.
|
||||
@@ -172,7 +189,22 @@ message RemotePeerConfig {
|
||||
|
||||
// Wireguard allowed IPs of a remote peer e.g. [10.30.30.1/32]
|
||||
repeated string allowedIps = 2;
|
||||
|
||||
// SSHConfig is a SSH config of the remote peer. SSHConfig.sshPubKey should be ignored because peer knows it's SSH key.
|
||||
SSHConfig sshConfig = 3;
|
||||
|
||||
}
|
||||
|
||||
// SSHConfig represents SSH configurations of a peer.
|
||||
message SSHConfig {
|
||||
// sshEnabled indicates whether a SSH server is enabled on this peer
|
||||
bool sshEnabled = 1;
|
||||
|
||||
// sshPubKey is a SSH public key of a peer to be added to authorized_hosts.
|
||||
// This property should be ignore if SSHConfig comes from PeerConfig.
|
||||
bytes sshPubKey = 2;
|
||||
}
|
||||
|
||||
// DeviceAuthorizationFlowRequest empty struct for future expansion
|
||||
message DeviceAuthorizationFlowRequest {}
|
||||
// DeviceAuthorizationFlow represents Device Authorization Flow information
|
||||
@@ -195,7 +227,23 @@ message ProviderConfig {
|
||||
// An IDP application client secret
|
||||
string ClientSecret = 2;
|
||||
// An IDP API domain
|
||||
string Domain =3;
|
||||
// Deprecated. Use a DeviceAuthEndpoint and TokenEndpoint
|
||||
string Domain = 3;
|
||||
// An Audience for validation
|
||||
string Audience = 4;
|
||||
// DeviceAuthEndpoint is an endpoint to request device authentication code.
|
||||
string DeviceAuthEndpoint = 5;
|
||||
// TokenEndpoint is an endpoint to request auth token.
|
||||
string TokenEndpoint = 6;
|
||||
}
|
||||
|
||||
// Route represents a route.Route object
|
||||
message Route {
|
||||
string ID = 1;
|
||||
string Network = 2;
|
||||
int64 NetworkType = 3;
|
||||
string Peer = 4;
|
||||
int64 Metric = 5;
|
||||
bool Masquerade = 6;
|
||||
string NetID = 7;
|
||||
}
|
||||
@@ -7,12 +7,13 @@ import (
|
||||
cacheStore "github.com/eko/gocache/v2/store"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -20,9 +21,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
PublicCategory = "public"
|
||||
PrivateCategory = "private"
|
||||
UnknownCategory = "unknown"
|
||||
PublicCategory = "public"
|
||||
PrivateCategory = "private"
|
||||
UnknownCategory = "unknown"
|
||||
CacheExpirationMax = 7 * 24 * 3600 * time.Second // 7 days
|
||||
CacheExpirationMin = 3 * 24 * 3600 * time.Second // 3 days
|
||||
)
|
||||
|
||||
type AccountManager interface {
|
||||
@@ -32,7 +35,7 @@ type AccountManager interface {
|
||||
accountId string,
|
||||
keyName string,
|
||||
keyType SetupKeyType,
|
||||
expiresIn *util.Duration,
|
||||
expiresIn time.Duration,
|
||||
) (*SetupKey, error)
|
||||
RevokeSetupKey(accountId string, keyId string) (*SetupKey, error)
|
||||
RenameSetupKey(accountId string, keyId string, newName string) (*SetupKey, error)
|
||||
@@ -41,18 +44,21 @@ type AccountManager interface {
|
||||
GetAccountWithAuthorizationClaims(claims jwtclaims.AuthorizationClaims) (*Account, error)
|
||||
IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error)
|
||||
AccountExists(accountId string) (*bool, error)
|
||||
AddAccount(accountId, userId, domain string) (*Account, error)
|
||||
GetPeer(peerKey string) (*Peer, error)
|
||||
MarkPeerConnected(peerKey string, connected bool) error
|
||||
RenamePeer(accountId string, peerKey string, newName string) (*Peer, error)
|
||||
DeletePeer(accountId string, peerKey string) (*Peer, error)
|
||||
GetPeerByIP(accountId string, peerIP string) (*Peer, error)
|
||||
UpdatePeer(accountID string, peer *Peer) (*Peer, error)
|
||||
GetNetworkMap(peerKey string) (*NetworkMap, error)
|
||||
GetPeerNetwork(peerKey string) (*Network, error)
|
||||
AddPeer(setupKey string, userId string, peer *Peer) (*Peer, error)
|
||||
UpdatePeerMeta(peerKey string, meta PeerSystemMeta) error
|
||||
UpdatePeerSSHKey(peerKey string, sshKey string) error
|
||||
GetUsersFromAccount(accountId string) ([]*UserInfo, error)
|
||||
GetGroup(accountId, groupID string) (*Group, error)
|
||||
SaveGroup(accountId string, group *Group) error
|
||||
UpdateGroup(accountID string, groupID string, operations []GroupUpdateOperation) (*Group, error)
|
||||
DeleteGroup(accountId, groupID string) error
|
||||
ListGroups(accountId string) ([]*Group, error)
|
||||
GroupAddPeer(accountId, groupID, peerKey string) error
|
||||
@@ -60,8 +66,15 @@ type AccountManager interface {
|
||||
GroupListPeers(accountId, groupID string) ([]*Peer, error)
|
||||
GetRule(accountId, ruleID string) (*Rule, error)
|
||||
SaveRule(accountID string, rule *Rule) error
|
||||
UpdateRule(accountID string, ruleID string, operations []RuleUpdateOperation) (*Rule, error)
|
||||
DeleteRule(accountId, ruleID string) error
|
||||
ListRules(accountId string) ([]*Rule, error)
|
||||
GetRoute(accountID, routeID string) (*route.Route, error)
|
||||
CreateRoute(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error)
|
||||
SaveRoute(accountID string, route *route.Route) error
|
||||
UpdateRoute(accountID string, routeID string, operations []RouteUpdateOperation) (*route.Route, error)
|
||||
DeleteRoute(accountID, routeID string) error
|
||||
ListRoutes(accountID string) ([]*route.Route, error)
|
||||
}
|
||||
|
||||
type DefaultAccountManager struct {
|
||||
@@ -88,6 +101,7 @@ type Account struct {
|
||||
Users map[string]*User
|
||||
Groups map[string]*Group
|
||||
Rules map[string]*Rule
|
||||
Routes map[string]*route.Route
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
@@ -97,12 +111,6 @@ type UserInfo struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
// NewAccount creates a new Account with a generated ID and generated default setup keys
|
||||
func NewAccount(userId, domain string) *Account {
|
||||
accountId := xid.New().String()
|
||||
return newAccountWithId(accountId, userId, domain)
|
||||
}
|
||||
|
||||
func (a *Account) Copy() *Account {
|
||||
peers := map[string]*Peer{}
|
||||
for id, peer := range a.Peers {
|
||||
@@ -162,23 +170,78 @@ func BuildManager(
|
||||
ctx: context.Background(),
|
||||
}
|
||||
|
||||
// if account has not default account
|
||||
// we build 'all' group and add all peers into it
|
||||
// also we create default rule with source an destination
|
||||
// groups 'all'
|
||||
// if account has not default group
|
||||
// we create 'all' group and add all peers into it
|
||||
// also we create default rule with source as destination
|
||||
for _, account := range store.GetAllAccounts() {
|
||||
am.addAllGroup(account)
|
||||
if err := store.SaveAccount(account); err != nil {
|
||||
_, err := account.GetGroupAll()
|
||||
if err != nil {
|
||||
addAllGroup(account)
|
||||
if err := store.SaveAccount(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gocacheClient := gocache.New(CacheExpirationMax, 30*time.Minute)
|
||||
gocacheStore := cacheStore.NewGoCache(gocacheClient, nil)
|
||||
|
||||
am.cacheManager = cache.NewLoadable(am.loadFromCache, cache.New(gocacheStore))
|
||||
|
||||
if !isNil(am.idpManager) {
|
||||
go func() {
|
||||
err := am.warmupIDPCache()
|
||||
if err != nil {
|
||||
log.Warnf("failed warming up cache due to error: %v", err)
|
||||
//todo retry?
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return am, nil
|
||||
|
||||
}
|
||||
|
||||
// newAccount creates a new Account with a generated ID and generated default setup keys.
|
||||
// If ID is already in use (due to collision) we try one more time before returning error
|
||||
func (am *DefaultAccountManager) newAccount(userID, domain string) (*Account, error) {
|
||||
for i := 0; i < 2; i++ {
|
||||
accountId := xid.New().String()
|
||||
|
||||
_, err := am.Store.GetAccount(accountId)
|
||||
statusErr, _ := status.FromError(err)
|
||||
if err == nil {
|
||||
log.Warnf("an account with ID already exists, retrying...")
|
||||
continue
|
||||
} else if statusErr.Code() == codes.NotFound {
|
||||
return newAccountWithId(accountId, userID, domain), nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
gocacheClient := gocache.New(7*24*time.Hour, 30*time.Minute)
|
||||
gocacheStore := cacheStore.NewGoCache(gocacheClient, nil)
|
||||
return nil, status.Errorf(codes.Internal, "error while creating new account")
|
||||
}
|
||||
|
||||
am.cacheManager = cache.NewLoadable(am.loadFromCache, cache.New(gocacheStore))
|
||||
return am, nil
|
||||
func (am *DefaultAccountManager) warmupIDPCache() error {
|
||||
userData, err := am.idpManager.GetAllAccounts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for accountID, users := range userData {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
r := rand.Intn(int(CacheExpirationMax.Milliseconds()-CacheExpirationMin.Milliseconds())) + int(CacheExpirationMin.Milliseconds())
|
||||
expiration := time.Duration(r) * time.Millisecond
|
||||
err = am.cacheManager.Set(am.ctx, accountID, users, &cacheStore.Options{Expiration: expiration})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Infof("warmed up IDP cache with %d entries", len(userData))
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSetupKey generates a new setup key with a given name and type, and adds it to the specified account
|
||||
@@ -186,14 +249,14 @@ func (am *DefaultAccountManager) AddSetupKey(
|
||||
accountId string,
|
||||
keyName string,
|
||||
keyType SetupKeyType,
|
||||
expiresIn *util.Duration,
|
||||
expiresIn time.Duration,
|
||||
) (*SetupKey, error) {
|
||||
am.mux.Lock()
|
||||
defer am.mux.Unlock()
|
||||
|
||||
keyDuration := DefaultSetupKeyDuration
|
||||
if expiresIn != nil {
|
||||
keyDuration = expiresIn.Duration
|
||||
if expiresIn != 0 {
|
||||
keyDuration = expiresIn
|
||||
}
|
||||
|
||||
account, err := am.Store.GetAccount(accountId)
|
||||
@@ -331,8 +394,8 @@ func mergeLocalAndQueryUser(queried idp.UserData, local User) *UserInfo {
|
||||
}
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) loadFromCache(ctx context.Context, accountID interface{}) (interface{}, error) {
|
||||
return am.idpManager.GetBatchedUserData(fmt.Sprintf("%v", accountID))
|
||||
func (am *DefaultAccountManager) loadFromCache(_ context.Context, accountID interface{}) (interface{}, error) {
|
||||
return am.idpManager.GetAccount(fmt.Sprintf("%v", accountID))
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) lookupCache(accountUsers map[string]*User, accountID string) ([]*idp.UserData, error) {
|
||||
@@ -421,8 +484,17 @@ func (am *DefaultAccountManager) updateAccountDomainAttributes(
|
||||
primaryDomain bool,
|
||||
) error {
|
||||
account.IsDomainPrimaryAccount = primaryDomain
|
||||
account.Domain = strings.ToLower(claims.Domain)
|
||||
account.DomainCategory = claims.DomainCategory
|
||||
|
||||
lowerDomain := strings.ToLower(claims.Domain)
|
||||
userObj := account.Users[claims.UserId]
|
||||
if account.Domain != lowerDomain && userObj.Role == UserRoleAdmin {
|
||||
account.Domain = lowerDomain
|
||||
}
|
||||
// prevent updating category for different domain until admin logs in
|
||||
if account.Domain == lowerDomain {
|
||||
account.DomainCategory = claims.DomainCategory
|
||||
}
|
||||
|
||||
err := am.Store.SaveAccount(account)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "failed saving updated account")
|
||||
@@ -486,8 +558,10 @@ func (am *DefaultAccountManager) handleNewUserAccount(
|
||||
return nil, status.Errorf(codes.Internal, "failed saving updated account")
|
||||
}
|
||||
} else {
|
||||
account = NewAccount(claims.UserId, lowerDomain)
|
||||
account.Users[claims.UserId] = NewAdminUser(claims.UserId)
|
||||
account, err = am.newAccount(claims.UserId, lowerDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = am.updateAccountDomainAttributes(account, claims, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -584,29 +658,8 @@ func (am *DefaultAccountManager) AccountExists(accountId string) (*bool, error)
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// AddAccount generates a new Account with a provided accountId and userId, saves to the Store
|
||||
func (am *DefaultAccountManager) AddAccount(accountId, userId, domain string) (*Account, error) {
|
||||
am.mux.Lock()
|
||||
defer am.mux.Unlock()
|
||||
|
||||
return am.createAccount(accountId, userId, domain)
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) createAccount(accountId, userId, domain string) (*Account, error) {
|
||||
account := newAccountWithId(accountId, userId, domain)
|
||||
|
||||
am.addAllGroup(account)
|
||||
|
||||
err := am.Store.SaveAccount(account)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed creating account")
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// addAllGroup to account object it it doesn't exists
|
||||
func (am *DefaultAccountManager) addAllGroup(account *Account) {
|
||||
// addAllGroup to account object if it doesn't exists
|
||||
func addAllGroup(account *Account) {
|
||||
if len(account.Groups) == 0 {
|
||||
allGroup := &Group{
|
||||
ID: xid.New().String(),
|
||||
@@ -619,7 +672,9 @@ func (am *DefaultAccountManager) addAllGroup(account *Account) {
|
||||
|
||||
defaultRule := &Rule{
|
||||
ID: xid.New().String(),
|
||||
Name: "Default",
|
||||
Name: DefaultRuleName,
|
||||
Description: DefaultRuleDescription,
|
||||
Disabled: false,
|
||||
Source: []string{allGroup.ID},
|
||||
Destination: []string{allGroup.ID},
|
||||
}
|
||||
@@ -639,10 +694,11 @@ func newAccountWithId(accountId, userId, domain string) *Account {
|
||||
network := NewNetwork()
|
||||
peers := make(map[string]*Peer)
|
||||
users := make(map[string]*User)
|
||||
|
||||
routes := make(map[string]*route.Route)
|
||||
users[userId] = NewAdminUser(userId)
|
||||
log.Debugf("created new account %s with setup key %s", accountId, defaultKey.Key)
|
||||
|
||||
return &Account{
|
||||
acc := &Account{
|
||||
Id: accountId,
|
||||
SetupKeys: setupKeys,
|
||||
Network: network,
|
||||
@@ -650,7 +706,11 @@ func newAccountWithId(accountId, userId, domain string) *Account {
|
||||
Users: users,
|
||||
CreatedBy: userId,
|
||||
Domain: domain,
|
||||
Routes: routes,
|
||||
}
|
||||
|
||||
addAllGroup(acc)
|
||||
return acc
|
||||
}
|
||||
|
||||
func getAccountSetupKeyById(acc *Account, keyId string) *SetupKey {
|
||||
@@ -670,3 +730,19 @@ func getAccountSetupKeyByKey(acc *Account, key string) *SetupKey {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeFromList(inputList []string, toRemove []string) []string {
|
||||
toRemoveMap := make(map[string]struct{})
|
||||
for _, item := range toRemove {
|
||||
toRemoveMap[item] = struct{}{}
|
||||
}
|
||||
|
||||
var resultList []string
|
||||
for _, item := range inputList {
|
||||
_, ok := toRemoveMap[item]
|
||||
if !ok {
|
||||
resultList = append(resultList, item)
|
||||
}
|
||||
}
|
||||
return resultList
|
||||
}
|
||||
|
||||
@@ -11,6 +11,96 @@ import (
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
func verifyCanAddPeerToAccount(t *testing.T, manager AccountManager, account *Account, userID string) {
|
||||
peer := &Peer{
|
||||
Key: "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=",
|
||||
Name: "test-host@netbird.io",
|
||||
Meta: PeerSystemMeta{
|
||||
Hostname: "test-host@netbird.io",
|
||||
GoOS: "linux",
|
||||
Kernel: "Linux",
|
||||
Core: "21.04",
|
||||
Platform: "x86_64",
|
||||
OS: "Ubuntu",
|
||||
WtVersion: "development",
|
||||
UIVersion: "development",
|
||||
},
|
||||
}
|
||||
|
||||
var setupKey string
|
||||
for _, key := range account.SetupKeys {
|
||||
setupKey = key.Key
|
||||
}
|
||||
|
||||
_, err := manager.AddPeer(setupKey, userID, peer)
|
||||
if err != nil {
|
||||
t.Error("expected to add new peer successfully after creating new account, but failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyNewAccountHasDefaultFields(t *testing.T, account *Account, createdBy string, domain string, expectedUsers []string) {
|
||||
if len(account.Peers) != 0 {
|
||||
t.Errorf("expected account to have len(Peers) = %v, got %v", 0, len(account.Peers))
|
||||
}
|
||||
|
||||
if len(account.SetupKeys) != 2 {
|
||||
t.Errorf("expected account to have len(SetupKeys) = %v, got %v", 2, len(account.SetupKeys))
|
||||
}
|
||||
|
||||
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
|
||||
if !ipNet.Contains(account.Network.Net.IP) {
|
||||
t.Errorf("expected account's Network to be a subnet of %v, got %v", ipNet.String(), account.Network.Net.String())
|
||||
}
|
||||
|
||||
g, err := account.GetGroupAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.Name != "All" {
|
||||
t.Errorf("expecting account to have group ALL added by default")
|
||||
}
|
||||
if len(account.Users) != len(expectedUsers) {
|
||||
t.Errorf("expecting account to have %d users, got %d", len(expectedUsers), len(account.Users))
|
||||
}
|
||||
|
||||
if account.Users[createdBy] == nil {
|
||||
t.Errorf("expecting account to have createdBy user %s in a user map ", createdBy)
|
||||
}
|
||||
|
||||
for _, expectedUserID := range expectedUsers {
|
||||
if account.Users[expectedUserID] == nil {
|
||||
t.Errorf("expecting account to have a user %s in a user map", expectedUserID)
|
||||
}
|
||||
}
|
||||
|
||||
if account.CreatedBy != createdBy {
|
||||
t.Errorf("expecting newly created account to be created by user %s, got %s", createdBy, account.CreatedBy)
|
||||
}
|
||||
|
||||
if account.Domain != domain {
|
||||
t.Errorf("expecting newly created account to have domain %s, got %s", domain, account.Domain)
|
||||
}
|
||||
|
||||
if len(account.Rules) != 1 {
|
||||
t.Errorf("expecting newly created account to have 1 rule, got %d", len(account.Rules))
|
||||
}
|
||||
|
||||
for _, rule := range account.Rules {
|
||||
if rule.Name != "Default" {
|
||||
t.Errorf("expecting newly created account to have Default rule, got %s", rule.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAccount(t *testing.T) {
|
||||
|
||||
domain := "netbird.io"
|
||||
userId := "account_creator"
|
||||
accountID := "account_id"
|
||||
account := newAccountWithId(accountID, userId, domain)
|
||||
verifyNewAccountHasDefaultFields(t, account, userId, domain, []string{userId})
|
||||
}
|
||||
|
||||
func TestAccountManager_GetOrCreateAccountByUser(t *testing.T) {
|
||||
manager, err := createManager(t)
|
||||
if err != nil {
|
||||
@@ -51,6 +141,8 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
||||
expectedUserRole UserRole
|
||||
expectedDomainCategory string
|
||||
expectedPrimaryDomainStatus bool
|
||||
expectedCreatedBy string
|
||||
expectedUsers []string
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -77,6 +169,8 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
||||
expectedUserRole: UserRoleAdmin,
|
||||
expectedDomainCategory: "",
|
||||
expectedPrimaryDomainStatus: false,
|
||||
expectedCreatedBy: "pub-domain-user",
|
||||
expectedUsers: []string{"pub-domain-user"},
|
||||
}
|
||||
|
||||
initUnknown := defaultInitAccount
|
||||
@@ -96,6 +190,8 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
||||
expectedUserRole: UserRoleAdmin,
|
||||
expectedDomainCategory: "",
|
||||
expectedPrimaryDomainStatus: false,
|
||||
expectedCreatedBy: "unknown-domain-user",
|
||||
expectedUsers: []string{"unknown-domain-user"},
|
||||
}
|
||||
|
||||
testCase3 := test{
|
||||
@@ -111,6 +207,8 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
||||
expectedUserRole: UserRoleAdmin,
|
||||
expectedDomainCategory: PrivateCategory,
|
||||
expectedPrimaryDomainStatus: true,
|
||||
expectedCreatedBy: "pvt-domain-user",
|
||||
expectedUsers: []string{"pvt-domain-user"},
|
||||
}
|
||||
|
||||
privateInitAccount := defaultInitAccount
|
||||
@@ -121,7 +219,7 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
||||
name: "New Regular User With Existing Private Domain",
|
||||
inputClaims: jwtclaims.AuthorizationClaims{
|
||||
Domain: privateDomain,
|
||||
UserId: "pvt-domain-user",
|
||||
UserId: "new-pvt-domain-user",
|
||||
DomainCategory: PrivateCategory,
|
||||
},
|
||||
inputUpdateAttrs: true,
|
||||
@@ -131,6 +229,8 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
||||
expectedUserRole: UserRoleUser,
|
||||
expectedDomainCategory: PrivateCategory,
|
||||
expectedPrimaryDomainStatus: true,
|
||||
expectedCreatedBy: defaultInitAccount.UserId,
|
||||
expectedUsers: []string{defaultInitAccount.UserId, "new-pvt-domain-user"},
|
||||
}
|
||||
|
||||
testCase5 := test{
|
||||
@@ -146,6 +246,8 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
||||
expectedUserRole: UserRoleAdmin,
|
||||
expectedDomainCategory: PrivateCategory,
|
||||
expectedPrimaryDomainStatus: true,
|
||||
expectedCreatedBy: defaultInitAccount.UserId,
|
||||
expectedUsers: []string{defaultInitAccount.UserId},
|
||||
}
|
||||
|
||||
testCase6 := test{
|
||||
@@ -162,6 +264,8 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
||||
expectedUserRole: UserRoleAdmin,
|
||||
expectedDomainCategory: PrivateCategory,
|
||||
expectedPrimaryDomainStatus: true,
|
||||
expectedCreatedBy: defaultInitAccount.UserId,
|
||||
expectedUsers: []string{defaultInitAccount.UserId},
|
||||
}
|
||||
for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5, testCase6} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
@@ -182,6 +286,8 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
||||
|
||||
account, err := manager.GetAccountWithAuthorizationClaims(testCase.inputClaims)
|
||||
require.NoError(t, err, "support function failed")
|
||||
verifyNewAccountHasDefaultFields(t, account, testCase.expectedCreatedBy, testCase.inputClaims.Domain, testCase.expectedUsers)
|
||||
verifyCanAddPeerToAccount(t, manager, account, testCase.expectedCreatedBy)
|
||||
|
||||
testCase.testingFunc(t, initAccount.Id, account.Id, testCase.expectedMSG)
|
||||
|
||||
@@ -255,41 +361,6 @@ func TestAccountManager_SetOrUpdateDomain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountManager_AddAccount(t *testing.T) {
|
||||
manager, err := createManager(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
expectedId := "test_account"
|
||||
userId := "account_creator"
|
||||
expectedPeersSize := 0
|
||||
expectedSetupKeysSize := 2
|
||||
|
||||
account, err := manager.AddAccount(expectedId, userId, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if account.Id != expectedId {
|
||||
t.Errorf("expected account to have Id = %s, got %s", expectedId, account.Id)
|
||||
}
|
||||
|
||||
if len(account.Peers) != expectedPeersSize {
|
||||
t.Errorf("expected account to have len(Peers) = %v, got %v", expectedPeersSize, len(account.Peers))
|
||||
}
|
||||
|
||||
if len(account.SetupKeys) != expectedSetupKeysSize {
|
||||
t.Errorf("expected account to have len(SetupKeys) = %v, got %v", expectedSetupKeysSize, len(account.SetupKeys))
|
||||
}
|
||||
|
||||
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
|
||||
if !ipNet.Contains(account.Network.Net.IP) {
|
||||
t.Errorf("expected account's Network to be a subnet of %v, got %v", ipNet.String(), account.Network.Net.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountManager_GetAccountByUserOrAccountId(t *testing.T) {
|
||||
manager, err := createManager(t)
|
||||
if err != nil {
|
||||
@@ -320,6 +391,15 @@ func TestAccountManager_GetAccountByUserOrAccountId(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func createAccount(am *DefaultAccountManager, accountID, userID, domain string) (*Account, error) {
|
||||
account := newAccountWithId(accountID, userID, domain)
|
||||
err := am.Store.SaveAccount(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func TestAccountManager_AccountExists(t *testing.T) {
|
||||
manager, err := createManager(t)
|
||||
if err != nil {
|
||||
@@ -329,7 +409,7 @@ func TestAccountManager_AccountExists(t *testing.T) {
|
||||
|
||||
expectedId := "test_account"
|
||||
userId := "account_creator"
|
||||
_, err = manager.AddAccount(expectedId, userId, "")
|
||||
_, err = createAccount(manager, expectedId, userId, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -353,7 +433,7 @@ func TestAccountManager_GetAccount(t *testing.T) {
|
||||
|
||||
expectedId := "test_account"
|
||||
userId := "account_creator"
|
||||
account, err := manager.AddAccount(expectedId, userId, "")
|
||||
account, err := createAccount(manager, expectedId, userId, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -389,7 +469,7 @@ func TestAccountManager_AddPeer(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
account, err := manager.AddAccount("test_account", "account_creator", "")
|
||||
account, err := createAccount(manager, "test_account", "account_creator", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -521,7 +601,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
account, err := manager.AddAccount("test_account", "account_creator", "")
|
||||
account, err := createAccount(manager, "test_account", "account_creator", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -704,7 +784,7 @@ func TestAccountManager_DeletePeer(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
account, err := manager.AddAccount("test_account", "account_creator", "")
|
||||
account, err := createAccount(manager, "test_account", "account_creator", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -757,7 +837,7 @@ func TestGetUsersFromAccount(t *testing.T) {
|
||||
users := map[string]*User{"1": {Id: "1", Role: "admin"}, "2": {Id: "2", Role: "user"}, "3": {Id: "3", Role: "user"}}
|
||||
accountId := "test_account_id"
|
||||
|
||||
account, err := manager.AddAccount(accountId, users["1"].Id, "")
|
||||
account, err := createAccount(manager, accountId, users["1"].Id, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -788,7 +868,7 @@ func TestAccountManager_UpdatePeerMeta(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
account, err := manager.AddAccount("test_account", "account_creator", "")
|
||||
account, err := createAccount(manager, "test_account", "account_creator", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const (
|
||||
TCP Protocol = "tcp"
|
||||
HTTP Protocol = "http"
|
||||
HTTPS Protocol = "https"
|
||||
AUTH0 Provider = "auth0"
|
||||
NONE Provider = "none"
|
||||
)
|
||||
|
||||
// Config of the Management service
|
||||
@@ -49,13 +49,14 @@ type HttpServerConfig struct {
|
||||
CertFile string
|
||||
//CertKey is the location of the certificate private key
|
||||
CertKey string
|
||||
Address string
|
||||
// AuthAudience identifies the recipients that the JWT is intended for (aud in JWT)
|
||||
AuthAudience string
|
||||
// AuthIssuer identifies principal that issued the JWT.
|
||||
AuthIssuer string
|
||||
// AuthKeysLocation is a location of JWT key set containing the public keys used to verify JWT
|
||||
AuthKeysLocation string
|
||||
// OIDCConfigEndpoint is the endpoint of an IDP manager to get OIDC configuration
|
||||
OIDCConfigEndpoint string
|
||||
}
|
||||
|
||||
// Host represents a Wiretrustee host (e.g. STUN, TURN, Signal)
|
||||
@@ -82,9 +83,14 @@ type ProviderConfig struct {
|
||||
// ClientSecret An IDP application client secret
|
||||
ClientSecret string
|
||||
// Domain An IDP API domain
|
||||
// Deprecated. Use TokenEndpoint and DeviceAuthEndpoint
|
||||
Domain string
|
||||
// Audience An Audience for to authorization validation
|
||||
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
|
||||
}
|
||||
|
||||
// validateURL validates input http url
|
||||
|
||||
@@ -2,6 +2,8 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -25,6 +27,8 @@ type FileStore struct {
|
||||
PrivateDomain2AccountId map[string]string `json:"-"`
|
||||
PeerKeyId2SrcRulesId map[string]map[string]struct{} `json:"-"`
|
||||
PeerKeyId2DstRulesId map[string]map[string]struct{} `json:"-"`
|
||||
PeerKeyID2RouteIDs map[string]map[string]struct{} `json:"-"`
|
||||
AccountPrefix2RouteIDs map[string]map[string][]string `json:"-"`
|
||||
|
||||
// mutex to synchronise Store read/write operations
|
||||
mux sync.Mutex `json:"-"`
|
||||
@@ -51,7 +55,9 @@ func restore(file string) (*FileStore, error) {
|
||||
UserId2AccountId: make(map[string]string),
|
||||
PrivateDomain2AccountId: make(map[string]string),
|
||||
PeerKeyId2SrcRulesId: make(map[string]map[string]struct{}),
|
||||
PeerKeyID2RouteIDs: make(map[string]map[string]struct{}),
|
||||
PeerKeyId2DstRulesId: make(map[string]map[string]struct{}),
|
||||
AccountPrefix2RouteIDs: make(map[string]map[string][]string),
|
||||
storeFile: file,
|
||||
}
|
||||
|
||||
@@ -74,8 +80,10 @@ func restore(file string) (*FileStore, error) {
|
||||
store.PeerKeyId2AccountId = make(map[string]string)
|
||||
store.UserId2AccountId = make(map[string]string)
|
||||
store.PrivateDomain2AccountId = make(map[string]string)
|
||||
store.PeerKeyId2SrcRulesId = map[string]map[string]struct{}{}
|
||||
store.PeerKeyId2DstRulesId = map[string]map[string]struct{}{}
|
||||
store.PeerKeyId2SrcRulesId = make(map[string]map[string]struct{})
|
||||
store.PeerKeyId2DstRulesId = make(map[string]map[string]struct{})
|
||||
store.PeerKeyID2RouteIDs = make(map[string]map[string]struct{})
|
||||
store.AccountPrefix2RouteIDs = make(map[string]map[string][]string)
|
||||
|
||||
for accountId, account := range store.Accounts {
|
||||
for setupKeyId := range account.SetupKeys {
|
||||
@@ -116,6 +124,25 @@ func restore(file string) (*FileStore, error) {
|
||||
for _, user := range account.Users {
|
||||
store.UserId2AccountId[user.Id] = accountId
|
||||
}
|
||||
for _, route := range account.Routes {
|
||||
if route.Peer == "" {
|
||||
continue
|
||||
}
|
||||
if store.PeerKeyID2RouteIDs[route.Peer] == nil {
|
||||
store.PeerKeyID2RouteIDs[route.Peer] = make(map[string]struct{})
|
||||
}
|
||||
store.PeerKeyID2RouteIDs[route.Peer][route.ID] = struct{}{}
|
||||
if store.AccountPrefix2RouteIDs[account.Id] == nil {
|
||||
store.AccountPrefix2RouteIDs[account.Id] = make(map[string][]string)
|
||||
}
|
||||
if _, ok := store.AccountPrefix2RouteIDs[account.Id][route.Network.String()]; !ok {
|
||||
store.AccountPrefix2RouteIDs[account.Id][route.Network.String()] = make([]string, 0)
|
||||
}
|
||||
store.AccountPrefix2RouteIDs[account.Id][route.Network.String()] = append(
|
||||
store.AccountPrefix2RouteIDs[account.Id][route.Network.String()],
|
||||
route.ID,
|
||||
)
|
||||
}
|
||||
if account.Domain != "" && account.DomainCategory == PrivateCategory &&
|
||||
account.IsDomainPrimaryAccount {
|
||||
store.PrivateDomain2AccountId[account.Domain] = accountId
|
||||
@@ -177,15 +204,16 @@ func (s *FileStore) DeletePeer(accountId string, peerKey string) (*Peer, error)
|
||||
if peer == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "peer not found")
|
||||
}
|
||||
|
||||
peerRoutes := s.PeerKeyID2RouteIDs[peerKey]
|
||||
delete(account.Peers, peerKey)
|
||||
delete(s.PeerKeyId2AccountId, peerKey)
|
||||
delete(s.PeerKeyId2DstRulesId, peerKey)
|
||||
delete(s.PeerKeyId2SrcRulesId, peerKey)
|
||||
delete(s.PeerKeyID2RouteIDs, peerKey)
|
||||
|
||||
// cleanup groups
|
||||
var peers []string
|
||||
for _, g := range account.Groups {
|
||||
var peers []string
|
||||
for _, p := range g.Peers {
|
||||
if p != peerKey {
|
||||
peers = append(peers, p)
|
||||
@@ -194,6 +222,11 @@ func (s *FileStore) DeletePeer(accountId string, peerKey string) (*Peer, error)
|
||||
g.Peers = peers
|
||||
}
|
||||
|
||||
for routeID := range peerRoutes {
|
||||
account.Routes[routeID].Enabled = false
|
||||
account.Routes[routeID].Peer = ""
|
||||
}
|
||||
|
||||
err = s.persist(s.storeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -238,10 +271,14 @@ func (s *FileStore) SaveAccount(account *Account) error {
|
||||
s.SetupKeyId2AccountId[strings.ToUpper(keyId)] = account.Id
|
||||
}
|
||||
|
||||
// enforce peer to account index and delete peer to route indexes for rebuild
|
||||
for _, peer := range account.Peers {
|
||||
s.PeerKeyId2AccountId[peer.Key] = account.Id
|
||||
delete(s.PeerKeyID2RouteIDs, peer.Key)
|
||||
}
|
||||
|
||||
delete(s.AccountPrefix2RouteIDs, account.Id)
|
||||
|
||||
// remove all peers related to account from rules indexes
|
||||
cleanIDs := make([]string, 0)
|
||||
for key := range s.PeerKeyId2SrcRulesId {
|
||||
@@ -294,6 +331,26 @@ func (s *FileStore) SaveAccount(account *Account) error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, route := range account.Routes {
|
||||
if route.Peer == "" {
|
||||
continue
|
||||
}
|
||||
if s.PeerKeyID2RouteIDs[route.Peer] == nil {
|
||||
s.PeerKeyID2RouteIDs[route.Peer] = make(map[string]struct{})
|
||||
}
|
||||
s.PeerKeyID2RouteIDs[route.Peer][route.ID] = struct{}{}
|
||||
if s.AccountPrefix2RouteIDs[account.Id] == nil {
|
||||
s.AccountPrefix2RouteIDs[account.Id] = make(map[string][]string)
|
||||
}
|
||||
if _, ok := s.AccountPrefix2RouteIDs[account.Id][route.Network.String()]; !ok {
|
||||
s.AccountPrefix2RouteIDs[account.Id][route.Network.String()] = make([]string, 0)
|
||||
}
|
||||
s.AccountPrefix2RouteIDs[account.Id][route.Network.String()] = append(
|
||||
s.AccountPrefix2RouteIDs[account.Id][route.Network.String()],
|
||||
route.ID,
|
||||
)
|
||||
}
|
||||
|
||||
for _, user := range account.Users {
|
||||
s.UserId2AccountId[user.Id] = account.Id
|
||||
}
|
||||
@@ -305,6 +362,7 @@ func (s *FileStore) SaveAccount(account *Account) error {
|
||||
return s.persist(s.storeFile)
|
||||
}
|
||||
|
||||
// GetAccountByPrivateDomain returns account by private domain
|
||||
func (s *FileStore) GetAccountByPrivateDomain(domain string) (*Account, error) {
|
||||
accountId, accountIdFound := s.PrivateDomain2AccountId[strings.ToLower(domain)]
|
||||
if !accountIdFound {
|
||||
@@ -322,6 +380,7 @@ func (s *FileStore) GetAccountByPrivateDomain(domain string) (*Account, error) {
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// GetAccountBySetupKey returns account by setup key id
|
||||
func (s *FileStore) GetAccountBySetupKey(setupKey string) (*Account, error) {
|
||||
accountId, accountIdFound := s.SetupKeyId2AccountId[strings.ToUpper(setupKey)]
|
||||
if !accountIdFound {
|
||||
@@ -336,6 +395,7 @@ func (s *FileStore) GetAccountBySetupKey(setupKey string) (*Account, error) {
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// GetAccountPeers returns account peers
|
||||
func (s *FileStore) GetAccountPeers(accountId string) ([]*Peer, error) {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
@@ -353,6 +413,7 @@ func (s *FileStore) GetAccountPeers(accountId string) ([]*Peer, error) {
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
// GetAllAccounts returns all accounts
|
||||
func (s *FileStore) GetAllAccounts() (all []*Account) {
|
||||
for _, a := range s.Accounts {
|
||||
all = append(all, a)
|
||||
@@ -361,6 +422,7 @@ func (s *FileStore) GetAllAccounts() (all []*Account) {
|
||||
return all
|
||||
}
|
||||
|
||||
// GetAccount returns an account for id
|
||||
func (s *FileStore) GetAccount(accountId string) (*Account, error) {
|
||||
account, accountFound := s.Accounts[accountId]
|
||||
if !accountFound {
|
||||
@@ -370,6 +432,7 @@ func (s *FileStore) GetAccount(accountId string) (*Account, error) {
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// GetUserAccount returns a user account
|
||||
func (s *FileStore) GetUserAccount(userId string) (*Account, error) {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
@@ -382,10 +445,7 @@ func (s *FileStore) GetUserAccount(userId string) (*Account, error) {
|
||||
return s.GetAccount(accountId)
|
||||
}
|
||||
|
||||
func (s *FileStore) GetPeerAccount(peerKey string) (*Account, error) {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
|
||||
func (s *FileStore) getPeerAccount(peerKey string) (*Account, error) {
|
||||
accountId, accountIdFound := s.PeerKeyId2AccountId[peerKey]
|
||||
if !accountIdFound {
|
||||
return nil, status.Errorf(codes.NotFound, "Provided peer key doesn't exists %s", peerKey)
|
||||
@@ -394,6 +454,15 @@ func (s *FileStore) GetPeerAccount(peerKey string) (*Account, error) {
|
||||
return s.GetAccount(accountId)
|
||||
}
|
||||
|
||||
// GetPeerAccount returns user account if exists
|
||||
func (s *FileStore) GetPeerAccount(peerKey string) (*Account, error) {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
|
||||
return s.getPeerAccount(peerKey)
|
||||
}
|
||||
|
||||
// GetPeerSrcRules return list of source rules for peer
|
||||
func (s *FileStore) GetPeerSrcRules(accountId, peerKey string) ([]*Rule, error) {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
@@ -419,6 +488,7 @@ func (s *FileStore) GetPeerSrcRules(accountId, peerKey string) ([]*Rule, error)
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
// GetPeerDstRules return list of destination rules for peer
|
||||
func (s *FileStore) GetPeerDstRules(accountId, peerKey string) ([]*Rule, error) {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
@@ -443,3 +513,56 @@ func (s *FileStore) GetPeerDstRules(accountId, peerKey string) ([]*Rule, error)
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
// GetPeerRoutes return list of routes for peer
|
||||
func (s *FileStore) GetPeerRoutes(peerKey string) ([]*route.Route, error) {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
|
||||
account, err := s.getPeerAccount(peerKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var routes []*route.Route
|
||||
|
||||
routeIDs, ok := s.PeerKeyID2RouteIDs[peerKey]
|
||||
if !ok {
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
for id := range routeIDs {
|
||||
route, found := account.Routes[id]
|
||||
if found {
|
||||
routes = append(routes, route)
|
||||
}
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// GetRoutesByPrefix return list of routes by account and route prefix
|
||||
func (s *FileStore) GetRoutesByPrefix(accountID string, prefix netip.Prefix) ([]*route.Route, error) {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
|
||||
account, err := s.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routeIDs, ok := s.AccountPrefix2RouteIDs[accountID][prefix.String()]
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.NotFound, "no routes for prefix: %v", prefix.String())
|
||||
}
|
||||
|
||||
var routes []*route.Route
|
||||
for _, id := range routeIDs {
|
||||
route, found := account.Routes[id]
|
||||
if found {
|
||||
routes = append(routes, route)
|
||||
}
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@ func TestNewStore(t *testing.T) {
|
||||
func TestSaveAccount(t *testing.T) {
|
||||
store := newStore(t)
|
||||
|
||||
account := NewAccount("testuser", "")
|
||||
account.Users["testuser"] = NewAdminUser("testuser")
|
||||
account := newAccountWithId("account_id", "testuser", "")
|
||||
setupKey := GenerateDefaultSetupKey()
|
||||
account.SetupKeys[setupKey.Key] = setupKey
|
||||
account.Peers["testpeer"] = &Peer{
|
||||
@@ -73,8 +72,7 @@ func TestSaveAccount(t *testing.T) {
|
||||
func TestStore(t *testing.T) {
|
||||
store := newStore(t)
|
||||
|
||||
account := NewAccount("testuser", "")
|
||||
account.Users["testuser"] = NewAdminUser("testuser")
|
||||
account := newAccountWithId("account_id", "testuser", "")
|
||||
account.Peers["testpeer"] = &Peer{
|
||||
Key: "peerkey",
|
||||
SetupKey: "peerkeysetupkey",
|
||||
|
||||
@@ -17,6 +17,26 @@ type Group struct {
|
||||
Peers []string
|
||||
}
|
||||
|
||||
const (
|
||||
// UpdateGroupName indicates a name update operation
|
||||
UpdateGroupName GroupUpdateOperationType = iota
|
||||
// InsertPeersToGroup indicates insert peers to group operation
|
||||
InsertPeersToGroup
|
||||
// RemovePeersFromGroup indicates a remove peers from group operation
|
||||
RemovePeersFromGroup
|
||||
// UpdateGroupPeers indicates a replacement of group peers list
|
||||
UpdateGroupPeers
|
||||
)
|
||||
|
||||
// GroupUpdateOperationType operation type
|
||||
type GroupUpdateOperationType int
|
||||
|
||||
// GroupUpdateOperation operation object with type and values to be applied
|
||||
type GroupUpdateOperation struct {
|
||||
Type GroupUpdateOperationType
|
||||
Values []string
|
||||
}
|
||||
|
||||
func (g *Group) Copy() *Group {
|
||||
return &Group{
|
||||
ID: g.ID,
|
||||
@@ -63,6 +83,56 @@ func (am *DefaultAccountManager) SaveGroup(accountID string, group *Group) error
|
||||
return am.updateAccountPeers(account)
|
||||
}
|
||||
|
||||
// UpdateGroup updates a group using a list of operations
|
||||
func (am *DefaultAccountManager) UpdateGroup(accountID string,
|
||||
groupID string, operations []GroupUpdateOperation) (*Group, error) {
|
||||
am.mux.Lock()
|
||||
defer am.mux.Unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.NotFound, "account not found")
|
||||
}
|
||||
|
||||
groupToUpdate, ok := account.Groups[groupID]
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.NotFound, "group %s no longer exists", groupID)
|
||||
}
|
||||
|
||||
group := groupToUpdate.Copy()
|
||||
|
||||
for _, operation := range operations {
|
||||
switch operation.Type {
|
||||
case UpdateGroupName:
|
||||
group.Name = operation.Values[0]
|
||||
case UpdateGroupPeers:
|
||||
group.Peers = operation.Values
|
||||
case InsertPeersToGroup:
|
||||
sourceList := group.Peers
|
||||
resultList := removeFromList(sourceList, operation.Values)
|
||||
group.Peers = append(resultList, operation.Values...)
|
||||
case RemovePeersFromGroup:
|
||||
sourceList := group.Peers
|
||||
resultList := removeFromList(sourceList, operation.Values)
|
||||
group.Peers = resultList
|
||||
}
|
||||
}
|
||||
|
||||
account.Groups[groupID] = group
|
||||
|
||||
account.Network.IncSerial()
|
||||
if err = am.Store.SaveAccount(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to update account peers")
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
// DeleteGroup object of the peers
|
||||
func (am *DefaultAccountManager) DeleteGroup(accountID, groupID string) error {
|
||||
am.mux.Lock()
|
||||
|
||||
@@ -3,6 +3,8 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
gPeer "google.golang.org/grpc/peer"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -18,8 +20,8 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// Server an instance of a Management server
|
||||
type Server struct {
|
||||
// GRPCServer an instance of a Management gRPC API server
|
||||
type GRPCServer struct {
|
||||
accountManager AccountManager
|
||||
wgKey wgtypes.Key
|
||||
proto.UnimplementedManagementServiceServer
|
||||
@@ -30,7 +32,7 @@ type Server struct {
|
||||
}
|
||||
|
||||
// NewServer creates a new Management server
|
||||
func NewServer(config *Config, accountManager AccountManager, peersUpdateManager *PeersUpdateManager, turnCredentialsManager TURNCredentialsManager) (*Server, error) {
|
||||
func NewServer(config *Config, accountManager AccountManager, peersUpdateManager *PeersUpdateManager, turnCredentialsManager TURNCredentialsManager) (*GRPCServer, error) {
|
||||
key, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -50,7 +52,7 @@ func NewServer(config *Config, accountManager AccountManager, peersUpdateManager
|
||||
log.Debug("unable to use http config to create new jwt middleware")
|
||||
}
|
||||
|
||||
return &Server{
|
||||
return &GRPCServer{
|
||||
wgKey: key,
|
||||
// peerKey -> event channel
|
||||
peersUpdateManager: peersUpdateManager,
|
||||
@@ -61,7 +63,7 @@ func NewServer(config *Config, accountManager AccountManager, peersUpdateManager
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetServerKey(ctx context.Context, req *proto.Empty) (*proto.ServerKeyResponse, error) {
|
||||
func (s *GRPCServer) GetServerKey(ctx context.Context, req *proto.Empty) (*proto.ServerKeyResponse, error) {
|
||||
// todo introduce something more meaningful with the key expiration/rotation
|
||||
now := time.Now().Add(24 * time.Hour)
|
||||
secs := int64(now.Second())
|
||||
@@ -76,7 +78,7 @@ func (s *Server) GetServerKey(ctx context.Context, req *proto.Empty) (*proto.Ser
|
||||
|
||||
// Sync validates the existence of a connecting peer, sends an initial state (all available for the connecting peers) and
|
||||
// notifies the connected peer of any updates (e.g. new peers under the same account)
|
||||
func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_SyncServer) error {
|
||||
func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_SyncServer) error {
|
||||
log.Debugf("Sync request from peer %s", req.WgPubKey)
|
||||
|
||||
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
|
||||
@@ -87,17 +89,24 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
||||
|
||||
peer, err := s.accountManager.GetPeer(peerKey.String())
|
||||
if err != nil {
|
||||
return status.Errorf(codes.PermissionDenied, "provided peer with the key wgPubKey %s is not registered", peerKey.String())
|
||||
p, _ := gPeer.FromContext(srv.Context())
|
||||
msg := status.Errorf(codes.PermissionDenied, "provided peer with the key wgPubKey %s is not registered, remote addr is %s", peerKey.String(), p.Addr.String())
|
||||
log.Debug(msg)
|
||||
return msg
|
||||
}
|
||||
|
||||
syncReq := &proto.SyncRequest{}
|
||||
err = encryption.DecryptMessage(peerKey, s.wgKey, req.Body, syncReq)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.InvalidArgument, "invalid request message")
|
||||
p, _ := gPeer.FromContext(srv.Context())
|
||||
msg := status.Errorf(codes.InvalidArgument, "invalid request message from %s,remote addr is %s", peerKey.String(), p.Addr.String())
|
||||
log.Debug(msg)
|
||||
return msg
|
||||
}
|
||||
|
||||
err = s.sendInitialSync(peerKey, peer, srv)
|
||||
if err != nil {
|
||||
log.Debugf("error while sending initial sync for %s: %v", peerKey.String(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -116,7 +125,7 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
||||
// condition when there are some updates
|
||||
case update, open := <-updates:
|
||||
if !open {
|
||||
// updates channel has been closed
|
||||
log.Debugf("updates channel for peer %s was closed", peerKey.String())
|
||||
return nil
|
||||
}
|
||||
log.Debugf("recevied an update for peer %s", peerKey.String())
|
||||
@@ -150,7 +159,7 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest) (*Peer, error) {
|
||||
func (s *GRPCServer) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest) (*Peer, error) {
|
||||
var (
|
||||
reqSetupKey string
|
||||
userId string
|
||||
@@ -185,9 +194,15 @@ func (s *Server) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest) (*Pe
|
||||
return nil, status.Errorf(codes.InvalidArgument, "peer meta data was not provided")
|
||||
}
|
||||
|
||||
var sshKey []byte
|
||||
if req.GetPeerKeys() != nil {
|
||||
sshKey = req.GetPeerKeys().GetSshPubKey()
|
||||
}
|
||||
|
||||
peer, err := s.accountManager.AddPeer(reqSetupKey, userId, &Peer{
|
||||
Key: peerKey.String(),
|
||||
Name: meta.GetHostname(),
|
||||
Key: peerKey.String(),
|
||||
Name: meta.GetHostname(),
|
||||
SSHKey: string(sshKey),
|
||||
Meta: PeerSystemMeta{
|
||||
Hostname: meta.GetHostname(),
|
||||
GoOS: meta.GetGoOS(),
|
||||
@@ -224,7 +239,7 @@ func (s *Server) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest) (*Pe
|
||||
peersToSend = append(peersToSend, p)
|
||||
}
|
||||
}
|
||||
update := toSyncResponse(s.config, remotePeer, peersToSend, nil, networkMap.Network.CurrentSerial())
|
||||
update := toSyncResponse(s.config, remotePeer, peersToSend, networkMap.Routes, nil, networkMap.Network.CurrentSerial(), networkMap.Network)
|
||||
err = s.peersUpdateManager.SendUpdate(remotePeer.Key, &UpdateMessage{Update: update})
|
||||
if err != nil {
|
||||
// todo rethink if we should keep this return
|
||||
@@ -239,7 +254,7 @@ func (s *Server) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest) (*Pe
|
||||
// In case it is, the login is successful
|
||||
// In case it isn't, the endpoint checks whether setup key is provided within the request and tries to register a peer.
|
||||
// In case of the successful registration login is also successful
|
||||
func (s *Server) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
|
||||
func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
|
||||
log.Debugf("Login request from peer %s", req.WgPubKey)
|
||||
|
||||
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
|
||||
@@ -259,8 +274,13 @@ func (s *Server) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto
|
||||
if errStatus, ok := status.FromError(err); ok && errStatus.Code() == codes.NotFound {
|
||||
// peer doesn't exist -> check if setup key was provided
|
||||
if loginReq.GetJwtToken() == "" && loginReq.GetSetupKey() == "" {
|
||||
// absent setup key -> permission denied
|
||||
return nil, status.Errorf(codes.PermissionDenied, "provided peer with the key wgPubKey %s is not registered and no setup key or jwt was provided", peerKey.String())
|
||||
// absent setup key or jwt -> permission denied
|
||||
p, _ := gPeer.FromContext(ctx)
|
||||
msg := status.Errorf(codes.PermissionDenied,
|
||||
"provided peer with the key wgPubKey %s is not registered and no setup key or jwt was provided,"+
|
||||
" remote addr is %s", peerKey.String(), p.Addr.String())
|
||||
log.Debug(msg)
|
||||
return nil, msg
|
||||
}
|
||||
|
||||
// setup key or jwt is present -> try normal registration flow
|
||||
@@ -290,10 +310,28 @@ func (s *Server) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto
|
||||
return nil, status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
}
|
||||
|
||||
var sshKey []byte
|
||||
if loginReq.GetPeerKeys() != nil {
|
||||
sshKey = loginReq.GetPeerKeys().GetSshPubKey()
|
||||
}
|
||||
|
||||
if len(sshKey) > 0 {
|
||||
err = s.accountManager.UpdatePeerSSHKey(peerKey.String(), string(sshKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
network, err := s.accountManager.GetPeerNetwork(peer.Key)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed getting peer network on login")
|
||||
}
|
||||
|
||||
// if peer has reached this point then it has logged in
|
||||
loginResp := &proto.LoginResponse{
|
||||
WiretrusteeConfig: toWiretrusteeConfig(s.config, nil),
|
||||
PeerConfig: toPeerConfig(peer),
|
||||
PeerConfig: toPeerConfig(peer, network),
|
||||
}
|
||||
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp)
|
||||
if err != nil {
|
||||
@@ -363,9 +401,11 @@ func toWiretrusteeConfig(config *Config, turnCredentials *TURNCredentials) *prot
|
||||
}
|
||||
}
|
||||
|
||||
func toPeerConfig(peer *Peer) *proto.PeerConfig {
|
||||
func toPeerConfig(peer *Peer, network *Network) *proto.PeerConfig {
|
||||
netmask, _ := network.Net.Mask.Size()
|
||||
return &proto.PeerConfig{
|
||||
Address: fmt.Sprintf("%s/%d", peer.IP.String(), SubnetSize), // take it from the network
|
||||
Address: fmt.Sprintf("%s/%d", peer.IP.String(), netmask), // take it from the network
|
||||
SshConfig: &proto.SSHConfig{SshEnabled: peer.SSHEnabled},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,19 +415,21 @@ func toRemotePeerConfig(peers []*Peer) []*proto.RemotePeerConfig {
|
||||
remotePeers = append(remotePeers, &proto.RemotePeerConfig{
|
||||
WgPubKey: rPeer.Key,
|
||||
AllowedIps: []string{fmt.Sprintf(AllowedIPsFormat, rPeer.IP)},
|
||||
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
|
||||
})
|
||||
}
|
||||
|
||||
return remotePeers
|
||||
}
|
||||
|
||||
func toSyncResponse(config *Config, peer *Peer, peers []*Peer, turnCredentials *TURNCredentials, serial uint64) *proto.SyncResponse {
|
||||
func toSyncResponse(config *Config, peer *Peer, peers []*Peer, routes []*route.Route, turnCredentials *TURNCredentials, serial uint64, network *Network) *proto.SyncResponse {
|
||||
wtConfig := toWiretrusteeConfig(config, turnCredentials)
|
||||
|
||||
pConfig := toPeerConfig(peer)
|
||||
pConfig := toPeerConfig(peer, network)
|
||||
|
||||
remotePeers := toRemotePeerConfig(peers)
|
||||
|
||||
routesUpdate := toProtocolRoutes(routes)
|
||||
|
||||
return &proto.SyncResponse{
|
||||
WiretrusteeConfig: wtConfig,
|
||||
PeerConfig: pConfig,
|
||||
@@ -398,17 +440,18 @@ func toSyncResponse(config *Config, peer *Peer, peers []*Peer, turnCredentials *
|
||||
PeerConfig: pConfig,
|
||||
RemotePeers: remotePeers,
|
||||
RemotePeersIsEmpty: len(remotePeers) == 0,
|
||||
Routes: routesUpdate,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// IsHealthy indicates whether the service is healthy
|
||||
func (s *Server) IsHealthy(ctx context.Context, req *proto.Empty) (*proto.Empty, error) {
|
||||
func (s *GRPCServer) IsHealthy(ctx context.Context, req *proto.Empty) (*proto.Empty, error) {
|
||||
return &proto.Empty{}, nil
|
||||
}
|
||||
|
||||
// sendInitialSync sends initial proto.SyncResponse to the peer requesting synchronization
|
||||
func (s *Server) sendInitialSync(peerKey wgtypes.Key, peer *Peer, srv proto.ManagementService_SyncServer) error {
|
||||
func (s *GRPCServer) sendInitialSync(peerKey wgtypes.Key, peer *Peer, srv proto.ManagementService_SyncServer) error {
|
||||
networkMap, err := s.accountManager.GetNetworkMap(peer.Key)
|
||||
if err != nil {
|
||||
log.Warnf("error getting a list of peers for a peer %s", peer.Key)
|
||||
@@ -423,7 +466,7 @@ func (s *Server) sendInitialSync(peerKey wgtypes.Key, peer *Peer, srv proto.Mana
|
||||
} else {
|
||||
turnCredentials = nil
|
||||
}
|
||||
plainResp := toSyncResponse(s.config, peer, networkMap.Peers, turnCredentials, networkMap.Network.CurrentSerial())
|
||||
plainResp := toSyncResponse(s.config, peer, networkMap.Peers, networkMap.Routes, turnCredentials, networkMap.Network.CurrentSerial(), networkMap.Network)
|
||||
|
||||
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, plainResp)
|
||||
if err != nil {
|
||||
@@ -446,7 +489,7 @@ func (s *Server) sendInitialSync(peerKey wgtypes.Key, peer *Peer, srv proto.Mana
|
||||
// GetDeviceAuthorizationFlow returns a device authorization flow information
|
||||
// This is used for initiating an Oauth 2 device authorization grant flow
|
||||
// which will be used by our clients to Login
|
||||
func (s *Server) GetDeviceAuthorizationFlow(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
|
||||
func (s *GRPCServer) GetDeviceAuthorizationFlow(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
|
||||
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
|
||||
if err != nil {
|
||||
errMSG := fmt.Sprintf("error while parsing peer's Wireguard public key %s on GetDeviceAuthorizationFlow request.", req.WgPubKey)
|
||||
@@ -461,7 +504,7 @@ func (s *Server) GetDeviceAuthorizationFlow(ctx context.Context, req *proto.Encr
|
||||
return nil, status.Error(codes.InvalidArgument, errMSG)
|
||||
}
|
||||
|
||||
if s.config.DeviceAuthorizationFlow == nil {
|
||||
if s.config.DeviceAuthorizationFlow == nil || s.config.DeviceAuthorizationFlow.Provider == string(NONE) {
|
||||
return nil, status.Error(codes.NotFound, "no device authorization flow information available")
|
||||
}
|
||||
|
||||
@@ -473,10 +516,12 @@ func (s *Server) GetDeviceAuthorizationFlow(ctx context.Context, req *proto.Encr
|
||||
flowInfoResp := &proto.DeviceAuthorizationFlow{
|
||||
Provider: proto.DeviceAuthorizationFlowProvider(provider),
|
||||
ProviderConfig: &proto.ProviderConfig{
|
||||
ClientID: s.config.DeviceAuthorizationFlow.ProviderConfig.ClientID,
|
||||
ClientSecret: s.config.DeviceAuthorizationFlow.ProviderConfig.ClientSecret,
|
||||
Domain: s.config.DeviceAuthorizationFlow.ProviderConfig.Domain,
|
||||
Audience: s.config.DeviceAuthorizationFlow.ProviderConfig.Audience,
|
||||
ClientID: s.config.DeviceAuthorizationFlow.ProviderConfig.ClientID,
|
||||
ClientSecret: s.config.DeviceAuthorizationFlow.ProviderConfig.ClientSecret,
|
||||
Domain: s.config.DeviceAuthorizationFlow.ProviderConfig.Domain,
|
||||
Audience: s.config.DeviceAuthorizationFlow.ProviderConfig.Audience,
|
||||
DeviceAuthEndpoint: s.config.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint,
|
||||
TokenEndpoint: s.config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
5
management/server/http/api/cfg.yaml
Normal file
5
management/server/http/api/cfg.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
package: api
|
||||
generate:
|
||||
models: true
|
||||
embedded-spec: false
|
||||
output: types.gen.go
|
||||
16
management/server/http/api/generate.sh
Normal file
16
management/server/http/api/generate.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/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 github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.11.0
|
||||
oapi-codegen --config cfg.yaml openapi.yml
|
||||
cd "$old_pwd"
|
||||
1183
management/server/http/api/openapi.yml
Normal file
1183
management/server/http/api/openapi.yml
Normal file
File diff suppressed because it is too large
Load Diff
470
management/server/http/api/types.gen.go
Normal file
470
management/server/http/api/types.gen.go
Normal file
@@ -0,0 +1,470 @@
|
||||
// Package api provides primitives to interact with the openapi HTTP API.
|
||||
//
|
||||
// Code generated by github.com/deepmap/oapi-codegen version v1.11.0 DO NOT EDIT.
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
BearerAuthScopes = "BearerAuth.Scopes"
|
||||
)
|
||||
|
||||
// Defines values for GroupPatchOperationOp.
|
||||
const (
|
||||
GroupPatchOperationOpAdd GroupPatchOperationOp = "add"
|
||||
GroupPatchOperationOpRemove GroupPatchOperationOp = "remove"
|
||||
GroupPatchOperationOpReplace GroupPatchOperationOp = "replace"
|
||||
)
|
||||
|
||||
// Defines values for GroupPatchOperationPath.
|
||||
const (
|
||||
GroupPatchOperationPathName GroupPatchOperationPath = "name"
|
||||
GroupPatchOperationPathPeers GroupPatchOperationPath = "peers"
|
||||
)
|
||||
|
||||
// Defines values for PatchMinimumOp.
|
||||
const (
|
||||
PatchMinimumOpAdd PatchMinimumOp = "add"
|
||||
PatchMinimumOpRemove PatchMinimumOp = "remove"
|
||||
PatchMinimumOpReplace PatchMinimumOp = "replace"
|
||||
)
|
||||
|
||||
// Defines values for RoutePatchOperationOp.
|
||||
const (
|
||||
RoutePatchOperationOpAdd RoutePatchOperationOp = "add"
|
||||
RoutePatchOperationOpRemove RoutePatchOperationOp = "remove"
|
||||
RoutePatchOperationOpReplace RoutePatchOperationOp = "replace"
|
||||
)
|
||||
|
||||
// Defines values for RoutePatchOperationPath.
|
||||
const (
|
||||
RoutePatchOperationPathDescription RoutePatchOperationPath = "description"
|
||||
RoutePatchOperationPathEnabled RoutePatchOperationPath = "enabled"
|
||||
RoutePatchOperationPathMasquerade RoutePatchOperationPath = "masquerade"
|
||||
RoutePatchOperationPathMetric RoutePatchOperationPath = "metric"
|
||||
RoutePatchOperationPathNetwork RoutePatchOperationPath = "network"
|
||||
RoutePatchOperationPathNetworkId RoutePatchOperationPath = "network_id"
|
||||
RoutePatchOperationPathPeer RoutePatchOperationPath = "peer"
|
||||
)
|
||||
|
||||
// Defines values for RulePatchOperationOp.
|
||||
const (
|
||||
RulePatchOperationOpAdd RulePatchOperationOp = "add"
|
||||
RulePatchOperationOpRemove RulePatchOperationOp = "remove"
|
||||
RulePatchOperationOpReplace RulePatchOperationOp = "replace"
|
||||
)
|
||||
|
||||
// Defines values for RulePatchOperationPath.
|
||||
const (
|
||||
RulePatchOperationPathDescription RulePatchOperationPath = "description"
|
||||
RulePatchOperationPathDestinations RulePatchOperationPath = "destinations"
|
||||
RulePatchOperationPathDisabled RulePatchOperationPath = "disabled"
|
||||
RulePatchOperationPathFlow RulePatchOperationPath = "flow"
|
||||
RulePatchOperationPathName RulePatchOperationPath = "name"
|
||||
RulePatchOperationPathSources RulePatchOperationPath = "sources"
|
||||
)
|
||||
|
||||
// Group defines model for Group.
|
||||
type Group struct {
|
||||
// Group ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// Group Name identifier
|
||||
Name string `json:"name"`
|
||||
|
||||
// List of peers object
|
||||
Peers []PeerMinimum `json:"peers"`
|
||||
|
||||
// Count of peers associated to the group
|
||||
PeersCount int `json:"peers_count"`
|
||||
}
|
||||
|
||||
// GroupMinimum defines model for GroupMinimum.
|
||||
type GroupMinimum struct {
|
||||
// Group ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// Group Name identifier
|
||||
Name string `json:"name"`
|
||||
|
||||
// Count of peers associated to the group
|
||||
PeersCount int `json:"peers_count"`
|
||||
}
|
||||
|
||||
// GroupPatchOperation defines model for GroupPatchOperation.
|
||||
type GroupPatchOperation struct {
|
||||
// Patch operation type
|
||||
Op GroupPatchOperationOp `json:"op"`
|
||||
|
||||
// Group field to update in form /<field>
|
||||
Path GroupPatchOperationPath `json:"path"`
|
||||
|
||||
// Values to be applied
|
||||
Value []string `json:"value"`
|
||||
}
|
||||
|
||||
// Patch operation type
|
||||
type GroupPatchOperationOp string
|
||||
|
||||
// Group field to update in form /<field>
|
||||
type GroupPatchOperationPath string
|
||||
|
||||
// PatchMinimum defines model for PatchMinimum.
|
||||
type PatchMinimum struct {
|
||||
// Patch operation type
|
||||
Op PatchMinimumOp `json:"op"`
|
||||
|
||||
// Values to be applied
|
||||
Value []string `json:"value"`
|
||||
}
|
||||
|
||||
// Patch operation type
|
||||
type PatchMinimumOp string
|
||||
|
||||
// Peer defines model for Peer.
|
||||
type Peer struct {
|
||||
// Provides information of who activated the Peer. User or Setup Key
|
||||
ActivatedBy struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
} `json:"activated_by"`
|
||||
|
||||
// Peer to Management connection status
|
||||
Connected bool `json:"connected"`
|
||||
|
||||
// Groups that the peer belongs to
|
||||
Groups []GroupMinimum `json:"groups"`
|
||||
|
||||
// Peer ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// Peer's IP address
|
||||
Ip string `json:"ip"`
|
||||
|
||||
// Last time peer connected to Netbird's management service
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
|
||||
// Peer's hostname
|
||||
Name string `json:"name"`
|
||||
|
||||
// Peer's operating system and version
|
||||
Os string `json:"os"`
|
||||
|
||||
// Indicates whether SSH server is enabled on this peer
|
||||
SshEnabled bool `json:"ssh_enabled"`
|
||||
|
||||
// Peer's daemon or cli version
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// PeerMinimum defines model for PeerMinimum.
|
||||
type PeerMinimum struct {
|
||||
// Peer ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// Peer's hostname
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Route defines model for Route.
|
||||
type Route struct {
|
||||
// Route description
|
||||
Description string `json:"description"`
|
||||
|
||||
// Route status
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Route Id
|
||||
Id string `json:"id"`
|
||||
|
||||
// Indicate if peer should masquerade traffic to this route's prefix
|
||||
Masquerade bool `json:"masquerade"`
|
||||
|
||||
// Route metric number. Lowest number has higher priority
|
||||
Metric int `json:"metric"`
|
||||
|
||||
// Network range in CIDR format
|
||||
Network string `json:"network"`
|
||||
|
||||
// Route network identifier, to group HA routes
|
||||
NetworkId string `json:"network_id"`
|
||||
|
||||
// Network type indicating if it is IPv4 or IPv6
|
||||
NetworkType string `json:"network_type"`
|
||||
|
||||
// Peer Identifier associated with route
|
||||
Peer string `json:"peer"`
|
||||
}
|
||||
|
||||
// RoutePatchOperation defines model for RoutePatchOperation.
|
||||
type RoutePatchOperation struct {
|
||||
// Patch operation type
|
||||
Op RoutePatchOperationOp `json:"op"`
|
||||
|
||||
// Route field to update in form /<field>
|
||||
Path RoutePatchOperationPath `json:"path"`
|
||||
|
||||
// Values to be applied
|
||||
Value []string `json:"value"`
|
||||
}
|
||||
|
||||
// Patch operation type
|
||||
type RoutePatchOperationOp string
|
||||
|
||||
// Route field to update in form /<field>
|
||||
type RoutePatchOperationPath string
|
||||
|
||||
// RouteRequest defines model for RouteRequest.
|
||||
type RouteRequest struct {
|
||||
// Route description
|
||||
Description string `json:"description"`
|
||||
|
||||
// Route status
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Indicate if peer should masquerade traffic to this route's prefix
|
||||
Masquerade bool `json:"masquerade"`
|
||||
|
||||
// Route metric number. Lowest number has higher priority
|
||||
Metric int `json:"metric"`
|
||||
|
||||
// Network range in CIDR format
|
||||
Network string `json:"network"`
|
||||
|
||||
// Route network identifier, to group HA routes
|
||||
NetworkId string `json:"network_id"`
|
||||
|
||||
// Peer Identifier associated with route
|
||||
Peer string `json:"peer"`
|
||||
}
|
||||
|
||||
// Rule defines model for Rule.
|
||||
type Rule struct {
|
||||
// Rule friendly description
|
||||
Description string `json:"description"`
|
||||
|
||||
// Rule destination groups
|
||||
Destinations []GroupMinimum `json:"destinations"`
|
||||
|
||||
// Rules status
|
||||
Disabled bool `json:"disabled"`
|
||||
|
||||
// Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
|
||||
Flow string `json:"flow"`
|
||||
|
||||
// Rule ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// Rule name identifier
|
||||
Name string `json:"name"`
|
||||
|
||||
// Rule source groups
|
||||
Sources []GroupMinimum `json:"sources"`
|
||||
}
|
||||
|
||||
// RuleMinimum defines model for RuleMinimum.
|
||||
type RuleMinimum struct {
|
||||
// Rule friendly description
|
||||
Description string `json:"description"`
|
||||
|
||||
// Rules status
|
||||
Disabled bool `json:"disabled"`
|
||||
|
||||
// Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
|
||||
Flow string `json:"flow"`
|
||||
|
||||
// Rule name identifier
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// RulePatchOperation defines model for RulePatchOperation.
|
||||
type RulePatchOperation struct {
|
||||
// Patch operation type
|
||||
Op RulePatchOperationOp `json:"op"`
|
||||
|
||||
// Rule field to update in form /<field>
|
||||
Path RulePatchOperationPath `json:"path"`
|
||||
|
||||
// Values to be applied
|
||||
Value []string `json:"value"`
|
||||
}
|
||||
|
||||
// Patch operation type
|
||||
type RulePatchOperationOp string
|
||||
|
||||
// Rule field to update in form /<field>
|
||||
type RulePatchOperationPath string
|
||||
|
||||
// SetupKey defines model for SetupKey.
|
||||
type SetupKey struct {
|
||||
// Setup Key expiration date
|
||||
Expires time.Time `json:"expires"`
|
||||
|
||||
// Setup Key ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// Setup Key value
|
||||
Key string `json:"key"`
|
||||
|
||||
// Setup key last usage date
|
||||
LastUsed time.Time `json:"last_used"`
|
||||
|
||||
// Setup key name identifier
|
||||
Name string `json:"name"`
|
||||
|
||||
// Setup key revocation status
|
||||
Revoked bool `json:"revoked"`
|
||||
|
||||
// Setup key status, "valid", "overused","expired" or "revoked"
|
||||
State string `json:"state"`
|
||||
|
||||
// Setup key type, one-off for single time usage and reusable
|
||||
Type string `json:"type"`
|
||||
|
||||
// Usage count of setup key
|
||||
UsedTimes int `json:"used_times"`
|
||||
|
||||
// Setup key validity status
|
||||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
// SetupKeyRequest defines model for SetupKeyRequest.
|
||||
type SetupKeyRequest struct {
|
||||
// Expiration time in seconds
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
|
||||
// Setup Key name
|
||||
Name string `json:"name"`
|
||||
|
||||
// Setup key revocation status
|
||||
Revoked bool `json:"revoked"`
|
||||
|
||||
// Setup key type, one-off for single time usage and reusable
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// User defines model for User.
|
||||
type User struct {
|
||||
// User's email address
|
||||
Email string `json:"email"`
|
||||
|
||||
// User ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// User's name from idp provider
|
||||
Name string `json:"name"`
|
||||
|
||||
// User's Netbird account role
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
// PostApiGroupsJSONBody defines parameters for PostApiGroups.
|
||||
type PostApiGroupsJSONBody struct {
|
||||
Name string `json:"name"`
|
||||
Peers *[]string `json:"peers,omitempty"`
|
||||
}
|
||||
|
||||
// PatchApiGroupsIdJSONBody defines parameters for PatchApiGroupsId.
|
||||
type PatchApiGroupsIdJSONBody = []GroupPatchOperation
|
||||
|
||||
// PutApiGroupsIdJSONBody defines parameters for PutApiGroupsId.
|
||||
type PutApiGroupsIdJSONBody struct {
|
||||
Name *string `json:"Name,omitempty"`
|
||||
Peers *[]string `json:"Peers,omitempty"`
|
||||
}
|
||||
|
||||
// PutApiPeersIdJSONBody defines parameters for PutApiPeersId.
|
||||
type PutApiPeersIdJSONBody struct {
|
||||
Name string `json:"name"`
|
||||
SshEnabled bool `json:"ssh_enabled"`
|
||||
}
|
||||
|
||||
// PostApiRoutesJSONBody defines parameters for PostApiRoutes.
|
||||
type PostApiRoutesJSONBody = RouteRequest
|
||||
|
||||
// PatchApiRoutesIdJSONBody defines parameters for PatchApiRoutesId.
|
||||
type PatchApiRoutesIdJSONBody = []RoutePatchOperation
|
||||
|
||||
// PutApiRoutesIdJSONBody defines parameters for PutApiRoutesId.
|
||||
type PutApiRoutesIdJSONBody = RouteRequest
|
||||
|
||||
// PostApiRulesJSONBody defines parameters for PostApiRules.
|
||||
type PostApiRulesJSONBody struct {
|
||||
// Rule friendly description
|
||||
Description string `json:"description"`
|
||||
Destinations *[]string `json:"destinations,omitempty"`
|
||||
|
||||
// Rules status
|
||||
Disabled bool `json:"disabled"`
|
||||
|
||||
// Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
|
||||
Flow string `json:"flow"`
|
||||
|
||||
// Rule name identifier
|
||||
Name string `json:"name"`
|
||||
Sources *[]string `json:"sources,omitempty"`
|
||||
}
|
||||
|
||||
// PatchApiRulesIdJSONBody defines parameters for PatchApiRulesId.
|
||||
type PatchApiRulesIdJSONBody = []RulePatchOperation
|
||||
|
||||
// PutApiRulesIdJSONBody defines parameters for PutApiRulesId.
|
||||
type PutApiRulesIdJSONBody struct {
|
||||
// Rule friendly description
|
||||
Description string `json:"description"`
|
||||
Destinations *[]string `json:"destinations,omitempty"`
|
||||
|
||||
// Rules status
|
||||
Disabled bool `json:"disabled"`
|
||||
|
||||
// Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
|
||||
Flow string `json:"flow"`
|
||||
|
||||
// Rule name identifier
|
||||
Name string `json:"name"`
|
||||
Sources *[]string `json:"sources,omitempty"`
|
||||
}
|
||||
|
||||
// PostApiSetupKeysJSONBody defines parameters for PostApiSetupKeys.
|
||||
type PostApiSetupKeysJSONBody = SetupKeyRequest
|
||||
|
||||
// PutApiSetupKeysIdJSONBody defines parameters for PutApiSetupKeysId.
|
||||
type PutApiSetupKeysIdJSONBody = SetupKeyRequest
|
||||
|
||||
// PostApiGroupsJSONRequestBody defines body for PostApiGroups for application/json ContentType.
|
||||
type PostApiGroupsJSONRequestBody PostApiGroupsJSONBody
|
||||
|
||||
// PatchApiGroupsIdJSONRequestBody defines body for PatchApiGroupsId for application/json ContentType.
|
||||
type PatchApiGroupsIdJSONRequestBody = PatchApiGroupsIdJSONBody
|
||||
|
||||
// PutApiGroupsIdJSONRequestBody defines body for PutApiGroupsId for application/json ContentType.
|
||||
type PutApiGroupsIdJSONRequestBody PutApiGroupsIdJSONBody
|
||||
|
||||
// PutApiPeersIdJSONRequestBody defines body for PutApiPeersId for application/json ContentType.
|
||||
type PutApiPeersIdJSONRequestBody PutApiPeersIdJSONBody
|
||||
|
||||
// PostApiRoutesJSONRequestBody defines body for PostApiRoutes for application/json ContentType.
|
||||
type PostApiRoutesJSONRequestBody = PostApiRoutesJSONBody
|
||||
|
||||
// PatchApiRoutesIdJSONRequestBody defines body for PatchApiRoutesId for application/json ContentType.
|
||||
type PatchApiRoutesIdJSONRequestBody = PatchApiRoutesIdJSONBody
|
||||
|
||||
// PutApiRoutesIdJSONRequestBody defines body for PutApiRoutesId for application/json ContentType.
|
||||
type PutApiRoutesIdJSONRequestBody = PutApiRoutesIdJSONBody
|
||||
|
||||
// PostApiRulesJSONRequestBody defines body for PostApiRules for application/json ContentType.
|
||||
type PostApiRulesJSONRequestBody PostApiRulesJSONBody
|
||||
|
||||
// PatchApiRulesIdJSONRequestBody defines body for PatchApiRulesId for application/json ContentType.
|
||||
type PatchApiRulesIdJSONRequestBody = PatchApiRulesIdJSONBody
|
||||
|
||||
// PutApiRulesIdJSONRequestBody defines body for PutApiRulesId for application/json ContentType.
|
||||
type PutApiRulesIdJSONRequestBody PutApiRulesIdJSONBody
|
||||
|
||||
// PostApiSetupKeysJSONRequestBody defines body for PostApiSetupKeys for application/json ContentType.
|
||||
type PostApiSetupKeysJSONRequestBody = PostApiSetupKeysJSONBody
|
||||
|
||||
// PutApiSetupKeysIdJSONRequestBody defines body for PutApiSetupKeysId for application/json ContentType.
|
||||
type PutApiSetupKeysIdJSONRequestBody = PutApiSetupKeysIdJSONBody
|
||||
371
management/server/http/groups.go
Normal file
371
management/server/http/groups.go
Normal file
@@ -0,0 +1,371 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"net/http"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/rs/xid"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Groups is a handler that returns groups of the account
|
||||
type Groups struct {
|
||||
jwtExtractor jwtclaims.ClaimsExtractor
|
||||
accountManager server.AccountManager
|
||||
authAudience string
|
||||
}
|
||||
|
||||
func NewGroups(accountManager server.AccountManager, authAudience string) *Groups {
|
||||
return &Groups{
|
||||
accountManager: accountManager,
|
||||
authAudience: authAudience,
|
||||
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllGroupsHandler list for the account
|
||||
func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var groups []*api.Group
|
||||
for _, g := range account.Groups {
|
||||
groups = append(groups, toGroupResponse(account, g))
|
||||
}
|
||||
|
||||
writeJSONObject(w, groups)
|
||||
}
|
||||
|
||||
// UpdateGroupHandler handles update to a group identified by a given ID
|
||||
func (h *Groups) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
groupID, ok := vars["id"]
|
||||
if !ok {
|
||||
http.Error(w, "group ID field is missing", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(groupID) == 0 {
|
||||
http.Error(w, "group ID can't be empty", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
_, ok = account.Groups[groupID]
|
||||
if !ok {
|
||||
http.Error(w, fmt.Sprintf("couldn't find group with ID %s", groupID), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
allGroup, err := account.GetGroupAll()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if allGroup.ID == groupID {
|
||||
http.Error(w, "updating group ALL is not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PutApiGroupsIdJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if *req.Name == "" {
|
||||
http.Error(w, "group name shouldn't be empty", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
group := server.Group{
|
||||
ID: groupID,
|
||||
Name: *req.Name,
|
||||
Peers: peerIPsToKeys(account, req.Peers),
|
||||
}
|
||||
|
||||
if err := h.accountManager.SaveGroup(account.Id, &group); err != nil {
|
||||
log.Errorf("failed updating group %s under account %s %v", groupID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, toGroupResponse(account, &group))
|
||||
}
|
||||
|
||||
// PatchGroupHandler handles patch updates to a group identified by a given ID
|
||||
func (h *Groups) PatchGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
groupID := vars["id"]
|
||||
if len(groupID) == 0 {
|
||||
http.Error(w, "invalid group Id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, ok := account.Groups[groupID]
|
||||
if !ok {
|
||||
http.Error(w, fmt.Sprintf("couldn't find group id %s", groupID), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
allGroup, err := account.GetGroupAll()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if allGroup.ID == groupID {
|
||||
http.Error(w, "updating group ALL is not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PatchApiGroupsIdJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req) == 0 {
|
||||
http.Error(w, "no patch instruction received", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var operations []server.GroupUpdateOperation
|
||||
|
||||
for _, patch := range req {
|
||||
switch patch.Path {
|
||||
case api.GroupPatchOperationPathName:
|
||||
if patch.Op != api.GroupPatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Name field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(patch.Value) == 0 || patch.Value[0] == "" {
|
||||
http.Error(w, "Group name shouldn't be empty", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
operations = append(operations, server.GroupUpdateOperation{
|
||||
Type: server.UpdateGroupName,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.GroupPatchOperationPathPeers:
|
||||
switch patch.Op {
|
||||
case api.GroupPatchOperationOpReplace:
|
||||
peerKeys := peerIPsToKeys(account, &patch.Value)
|
||||
operations = append(operations, server.GroupUpdateOperation{
|
||||
Type: server.UpdateGroupPeers,
|
||||
Values: peerKeys,
|
||||
})
|
||||
case api.GroupPatchOperationOpRemove:
|
||||
peerKeys := peerIPsToKeys(account, &patch.Value)
|
||||
operations = append(operations, server.GroupUpdateOperation{
|
||||
Type: server.RemovePeersFromGroup,
|
||||
Values: peerKeys,
|
||||
})
|
||||
case api.GroupPatchOperationOpAdd:
|
||||
peerKeys := peerIPsToKeys(account, &patch.Value)
|
||||
operations = append(operations, server.GroupUpdateOperation{
|
||||
Type: server.InsertPeersToGroup,
|
||||
Values: peerKeys,
|
||||
})
|
||||
default:
|
||||
http.Error(w, "invalid operation, \"%s\", for Peers field", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
default:
|
||||
http.Error(w, "invalid patch path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
group, err := h.accountManager.UpdateGroup(account.Id, groupID, operations)
|
||||
|
||||
if err != nil {
|
||||
errStatus, ok := status.FromError(err)
|
||||
if ok && errStatus.Code() == codes.Internal {
|
||||
http.Error(w, errStatus.String(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if ok && errStatus.Code() == codes.NotFound {
|
||||
http.Error(w, errStatus.String(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
log.Errorf("failed updating group %s under account %s %v", groupID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, toGroupResponse(account, group))
|
||||
}
|
||||
|
||||
// CreateGroupHandler handles group creation request
|
||||
func (h *Groups) CreateGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PostApiGroupsJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
http.Error(w, "Group name shouldn't be empty", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
group := server.Group{
|
||||
ID: xid.New().String(),
|
||||
Name: req.Name,
|
||||
Peers: peerIPsToKeys(account, req.Peers),
|
||||
}
|
||||
|
||||
if err := h.accountManager.SaveGroup(account.Id, &group); err != nil {
|
||||
log.Errorf("failed creating group \"%s\" under account %s %v", req.Name, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, toGroupResponse(account, &group))
|
||||
}
|
||||
|
||||
// DeleteGroupHandler handles group deletion request
|
||||
func (h *Groups) DeleteGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
aID := account.Id
|
||||
|
||||
groupID := mux.Vars(r)["id"]
|
||||
if len(groupID) == 0 {
|
||||
http.Error(w, "invalid group ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
allGroup, err := account.GetGroupAll()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if allGroup.ID == groupID {
|
||||
http.Error(w, "deleting group ALL is not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.accountManager.DeleteGroup(aID, groupID); err != nil {
|
||||
log.Errorf("failed delete group %s under account %s %v", groupID, aID, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, "")
|
||||
}
|
||||
|
||||
// GetGroupHandler returns a group
|
||||
func (h *Groups) GetGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
groupID := mux.Vars(r)["id"]
|
||||
if len(groupID) == 0 {
|
||||
http.Error(w, "invalid group ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
group, err := h.accountManager.GetGroup(account.Id, groupID)
|
||||
if err != nil {
|
||||
http.Error(w, "group not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, toGroupResponse(account, group))
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func peerIPsToKeys(account *server.Account, peerIPs *[]string) []string {
|
||||
var mappedPeerKeys []string
|
||||
if peerIPs == nil {
|
||||
return mappedPeerKeys
|
||||
}
|
||||
|
||||
peersChecked := make(map[string]struct{})
|
||||
|
||||
for _, requestPeersIP := range *peerIPs {
|
||||
_, ok := peersChecked[requestPeersIP]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
peersChecked[requestPeersIP] = struct{}{}
|
||||
for _, accountPeer := range account.Peers {
|
||||
if accountPeer.IP.String() == requestPeersIP {
|
||||
mappedPeerKeys = append(mappedPeerKeys, accountPeer.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return mappedPeerKeys
|
||||
}
|
||||
|
||||
func toGroupResponse(account *server.Account, group *server.Group) *api.Group {
|
||||
cache := make(map[string]api.PeerMinimum)
|
||||
gr := api.Group{
|
||||
Id: group.ID,
|
||||
Name: group.Name,
|
||||
PeersCount: len(group.Peers),
|
||||
}
|
||||
|
||||
for _, pid := range group.Peers {
|
||||
_, ok := cache[pid]
|
||||
if !ok {
|
||||
peer, ok := account.Peers[pid]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
peerResp := api.PeerMinimum{
|
||||
Id: peer.IP.String(),
|
||||
Name: peer.Name,
|
||||
}
|
||||
cache[pid] = peerResp
|
||||
gr.Peers = append(gr.Peers, peerResp)
|
||||
}
|
||||
}
|
||||
return &gr
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package handler
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -18,6 +20,11 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||
)
|
||||
|
||||
var TestPeers = map[string]*server.Peer{
|
||||
"A": &server.Peer{Key: "A", IP: net.ParseIP("100.100.100.100")},
|
||||
"B": &server.Peer{Key: "B", IP: net.ParseIP("200.200.200.200")},
|
||||
}
|
||||
|
||||
func initGroupTestData(groups ...*server.Group) *Groups {
|
||||
return &Groups{
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
@@ -36,10 +43,38 @@ func initGroupTestData(groups ...*server.Group) *Groups {
|
||||
Name: "Group",
|
||||
}, nil
|
||||
},
|
||||
UpdateGroupFunc: func(_ string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error) {
|
||||
var group server.Group
|
||||
group.ID = groupID
|
||||
for _, operation := range operations {
|
||||
switch operation.Type {
|
||||
case server.UpdateGroupName:
|
||||
group.Name = operation.Values[0]
|
||||
case server.UpdateGroupPeers, server.InsertPeersToGroup:
|
||||
group.Peers = operation.Values
|
||||
case server.RemovePeersFromGroup:
|
||||
default:
|
||||
return nil, fmt.Errorf("no operation")
|
||||
}
|
||||
}
|
||||
return &group, nil
|
||||
},
|
||||
GetPeerByIPFunc: func(_ string, peerIP string) (*server.Peer, error) {
|
||||
for _, peer := range TestPeers {
|
||||
if peer.IP.String() == peerIP {
|
||||
return peer, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("peer not found")
|
||||
},
|
||||
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
return &server.Account{
|
||||
Id: claims.AccountId,
|
||||
Domain: "hotmail.com",
|
||||
Peers: TestPeers,
|
||||
Groups: map[string]*server.Group{
|
||||
"id-existed": &server.Group{ID: "id-existed", Peers: []string{"A", "B"}},
|
||||
"id-all": &server.Group{ID: "id-all", Name: "All"}},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
@@ -125,41 +160,114 @@ func TestGetGroup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveGroup(t *testing.T) {
|
||||
func TestWriteGroup(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
expectedBody bool
|
||||
expectedGroup *server.Group
|
||||
expectedGroup *api.Group
|
||||
requestType string
|
||||
requestPath string
|
||||
requestBody io.Reader
|
||||
}{
|
||||
{
|
||||
name: "SaveGroup POST OK",
|
||||
name: "Write Group POST OK",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/groups",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`{"Name":"Default POSTed Group"}`)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedGroup: &server.Group{
|
||||
ID: "id-was-set",
|
||||
expectedGroup: &api.Group{
|
||||
Id: "id-was-set",
|
||||
Name: "Default POSTed Group",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SaveGroup PUT OK",
|
||||
requestType: http.MethodPut,
|
||||
name: "Write Group POST Invalid Name",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/groups",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`{"ID":"id-existed","Name":"Default POSTed Group"}`)),
|
||||
[]byte(`{"name":""}`)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Write Group PUT OK",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/groups/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`{"Name":"Default POSTed Group"}`)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedGroup: &server.Group{
|
||||
ID: "id-existed",
|
||||
expectedGroup: &api.Group{
|
||||
Id: "id-existed",
|
||||
Name: "Default POSTed Group",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Write Group PUT Invalid Name",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/groups/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`{"Name":""}`)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Write Group PUT All Group Name",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/groups/id-all",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`{"Name":"super"}`)),
|
||||
expectedStatus: http.StatusMethodNotAllowed,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Write Group PATCH Name OK",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/groups/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`[{"op":"replace","path":"name","value":["Default POSTed Group"]}]`)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedGroup: &api.Group{
|
||||
Id: "id-existed",
|
||||
Name: "Default POSTed Group",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Write Group PATCH Invalid Name OP",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/groups/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`[{"op":"insert","path":"name","value":[""]}]`)),
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Write Group PATCH Invalid Name",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/groups/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`[{"op":"replace","path":"name","value":[]}]`)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Write Group PATCH Peers OK",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/groups/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`[{"op":"replace","path":"peers","value":["100.100.100.100","200.200.200.200"]}]`)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedGroup: &api.Group{
|
||||
Id: "id-existed",
|
||||
PeersCount: 2,
|
||||
Peers: []api.PeerMinimum{
|
||||
{Id: "100.100.100.100"},
|
||||
{Id: "200.200.200.200"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p := initGroupTestData()
|
||||
@@ -170,7 +278,9 @@ func TestSaveGroup(t *testing.T) {
|
||||
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/api/groups", p.CreateOrUpdateGroupHandler).Methods("PUT", "POST")
|
||||
router.HandleFunc("/api/groups", p.CreateGroupHandler).Methods("POST")
|
||||
router.HandleFunc("/api/groups/{id}", p.UpdateGroupHandler).Methods("PUT")
|
||||
router.HandleFunc("/api/groups/{id}", p.PatchGroupHandler).Methods("PATCH")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
@@ -191,11 +301,10 @@ func TestSaveGroup(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
got := &server.Group{}
|
||||
got := &api.Group{}
|
||||
if err = json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, got, tc.expectedGroup)
|
||||
})
|
||||
}
|
||||
72
management/server/http/handler.go
Normal file
72
management/server/http/handler.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
s "github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http/middleware"
|
||||
"github.com/rs/cors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// APIHandler creates the Management service HTTP API handler registering all the available endpoints.
|
||||
func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience string, authKeysLocation string) (http.Handler, error) {
|
||||
jwtMiddleware, err := middleware.NewJwtMiddleware(
|
||||
authIssuer,
|
||||
authAudience,
|
||||
authKeysLocation,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
corsMiddleware := cors.AllowAll()
|
||||
|
||||
acMiddleware := middleware.NewAccessControll(
|
||||
authAudience,
|
||||
accountManager.IsUserAdmin)
|
||||
|
||||
apiHandler := mux.NewRouter()
|
||||
apiHandler.Use(corsMiddleware.Handler, jwtMiddleware.Handler, acMiddleware.Handler)
|
||||
|
||||
groupsHandler := NewGroups(accountManager, authAudience)
|
||||
rulesHandler := NewRules(accountManager, authAudience)
|
||||
peersHandler := NewPeers(accountManager, authAudience)
|
||||
keysHandler := NewSetupKeysHandler(accountManager, authAudience)
|
||||
userHandler := NewUserHandler(accountManager, authAudience)
|
||||
routesHandler := NewRoutes(accountManager, authAudience)
|
||||
|
||||
apiHandler.HandleFunc("/api/peers", peersHandler.GetPeers).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/peers/{id}", peersHandler.HandlePeer).
|
||||
Methods("GET", "PUT", "DELETE", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/users", userHandler.GetUsers).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("GET", "POST", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/setup-keys/{id}", keysHandler.HandleKey).Methods("GET", "PUT", "OPTIONS")
|
||||
|
||||
apiHandler.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("POST", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/setup-keys/{id}", keysHandler.HandleKey).
|
||||
Methods("GET", "PUT", "DELETE", "OPTIONS")
|
||||
|
||||
apiHandler.HandleFunc("/api/rules", rulesHandler.GetAllRulesHandler).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/rules", rulesHandler.CreateRuleHandler).Methods("POST", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/rules/{id}", rulesHandler.UpdateRuleHandler).Methods("PUT", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/rules/{id}", rulesHandler.PatchRuleHandler).Methods("PATCH", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/rules/{id}", rulesHandler.GetRuleHandler).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/rules/{id}", rulesHandler.DeleteRuleHandler).Methods("DELETE", "OPTIONS")
|
||||
|
||||
apiHandler.HandleFunc("/api/groups", groupsHandler.GetAllGroupsHandler).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/groups", groupsHandler.CreateGroupHandler).Methods("POST", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/groups/{id}", groupsHandler.UpdateGroupHandler).Methods("PUT", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/groups/{id}", groupsHandler.PatchGroupHandler).Methods("PATCH", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/groups/{id}", groupsHandler.GetGroupHandler).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/groups/{id}", groupsHandler.DeleteGroupHandler).Methods("DELETE", "OPTIONS")
|
||||
|
||||
apiHandler.HandleFunc("/api/routes", routesHandler.GetAllRoutesHandler).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/routes", routesHandler.CreateRouteHandler).Methods("POST", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.UpdateRouteHandler).Methods("PUT", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.PatchRouteHandler).Methods("PATCH", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.GetRouteHandler).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.DeleteRouteHandler).Methods("DELETE", "OPTIONS")
|
||||
|
||||
return apiHandler, nil
|
||||
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/rs/xid"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GroupResponse is a response sent to the client
|
||||
type GroupResponse struct {
|
||||
ID string
|
||||
Name string
|
||||
Peers []GroupPeerResponse `json:",omitempty"`
|
||||
}
|
||||
|
||||
// GroupPeerResponse is a response sent to the client
|
||||
type GroupPeerResponse struct {
|
||||
Key string
|
||||
Name string
|
||||
}
|
||||
|
||||
// GroupRequest to create or update group
|
||||
type GroupRequest struct {
|
||||
ID string
|
||||
Name string
|
||||
Peers []string
|
||||
}
|
||||
|
||||
// Groups is a handler that returns groups of the account
|
||||
type Groups struct {
|
||||
jwtExtractor jwtclaims.ClaimsExtractor
|
||||
accountManager server.AccountManager
|
||||
authAudience string
|
||||
}
|
||||
|
||||
func NewGroups(accountManager server.AccountManager, authAudience string) *Groups {
|
||||
return &Groups{
|
||||
accountManager: accountManager,
|
||||
authAudience: authAudience,
|
||||
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllGroupsHandler list for the account
|
||||
func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := h.getGroupAccount(r)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var groups []*GroupResponse
|
||||
for _, g := range account.Groups {
|
||||
groups = append(groups, toGroupResponse(account, g))
|
||||
}
|
||||
|
||||
writeJSONObject(w, groups)
|
||||
}
|
||||
|
||||
func (h *Groups) CreateOrUpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := h.getGroupAccount(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req GroupRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
req.ID = xid.New().String()
|
||||
}
|
||||
|
||||
group := server.Group{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
Peers: req.Peers,
|
||||
}
|
||||
|
||||
if err := h.accountManager.SaveGroup(account.Id, &group); err != nil {
|
||||
log.Errorf("failed updating group %s under account %s %v", req.ID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, toGroupResponse(account, &group))
|
||||
}
|
||||
|
||||
func (h *Groups) DeleteGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := h.getGroupAccount(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
aID := account.Id
|
||||
|
||||
gID := mux.Vars(r)["id"]
|
||||
if len(gID) == 0 {
|
||||
http.Error(w, "invalid group ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.accountManager.DeleteGroup(aID, gID); err != nil {
|
||||
log.Errorf("failed delete group %s under account %s %v", gID, aID, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, "")
|
||||
}
|
||||
|
||||
func (h *Groups) GetGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := h.getGroupAccount(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
groupID := mux.Vars(r)["id"]
|
||||
if len(groupID) == 0 {
|
||||
http.Error(w, "invalid group ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
group, err := h.accountManager.GetGroup(account.Id, groupID)
|
||||
if err != nil {
|
||||
http.Error(w, "group not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, toGroupResponse(account, group))
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Groups) getGroupAccount(r *http.Request) (*server.Account, error) {
|
||||
jwtClaims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
|
||||
|
||||
account, err := h.accountManager.GetAccountWithAuthorizationClaims(jwtClaims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func toGroupResponse(account *server.Account, group *server.Group) *GroupResponse {
|
||||
cache := make(map[string]GroupPeerResponse)
|
||||
gr := GroupResponse{
|
||||
ID: group.ID,
|
||||
Name: group.Name,
|
||||
}
|
||||
|
||||
for _, pid := range group.Peers {
|
||||
peerResp, ok := cache[pid]
|
||||
if !ok {
|
||||
peer, ok := account.Peers[pid]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
peerResp = GroupPeerResponse{
|
||||
Key: peer.Key,
|
||||
Name: peer.Name,
|
||||
}
|
||||
cache[pid] = peerResp
|
||||
}
|
||||
gr.Peers = append(gr.Peers, peerResp)
|
||||
}
|
||||
|
||||
return &gr
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/rs/xid"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const FlowBidirectString = "bidirect"
|
||||
|
||||
// RuleResponse is a response sent to the client
|
||||
type RuleResponse struct {
|
||||
ID string
|
||||
Name string
|
||||
Source []RuleGroupResponse
|
||||
Destination []RuleGroupResponse
|
||||
Flow string
|
||||
}
|
||||
|
||||
// RuleGroupResponse is a response sent to the client
|
||||
type RuleGroupResponse struct {
|
||||
ID string
|
||||
Name string
|
||||
PeersCount int
|
||||
}
|
||||
|
||||
// RuleRequest to create or update rule
|
||||
type RuleRequest struct {
|
||||
ID string
|
||||
Name string
|
||||
Source []string
|
||||
Destination []string
|
||||
Flow string
|
||||
}
|
||||
|
||||
// Rules is a handler that returns rules of the account
|
||||
type Rules struct {
|
||||
jwtExtractor jwtclaims.ClaimsExtractor
|
||||
accountManager server.AccountManager
|
||||
authAudience string
|
||||
}
|
||||
|
||||
func NewRules(accountManager server.AccountManager, authAudience string) *Rules {
|
||||
return &Rules{
|
||||
accountManager: accountManager,
|
||||
authAudience: authAudience,
|
||||
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllRulesHandler list for the account
|
||||
func (h *Rules) GetAllRulesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := h.getRuleAccount(r)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rules := []*RuleResponse{}
|
||||
for _, r := range account.Rules {
|
||||
rules = append(rules, toRuleResponse(account, r))
|
||||
}
|
||||
|
||||
writeJSONObject(w, rules)
|
||||
}
|
||||
|
||||
func (h *Rules) CreateOrUpdateRuleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := h.getRuleAccount(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req RuleRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
req.ID = xid.New().String()
|
||||
}
|
||||
|
||||
rule := server.Rule{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
Source: req.Source,
|
||||
Destination: req.Destination,
|
||||
}
|
||||
|
||||
switch req.Flow {
|
||||
case FlowBidirectString:
|
||||
rule.Flow = server.TrafficFlowBidirect
|
||||
default:
|
||||
http.Error(w, "unknown flow type", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.accountManager.SaveRule(account.Id, &rule); err != nil {
|
||||
log.Errorf("failed updating rule %s under account %s %v", req.ID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, &req)
|
||||
}
|
||||
|
||||
func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := h.getRuleAccount(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
aID := account.Id
|
||||
|
||||
rID := mux.Vars(r)["id"]
|
||||
if len(rID) == 0 {
|
||||
http.Error(w, "invalid rule ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.accountManager.DeleteRule(aID, rID); err != nil {
|
||||
log.Errorf("failed delete rule %s under account %s %v", rID, aID, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, "")
|
||||
}
|
||||
|
||||
func (h *Rules) GetRuleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := h.getRuleAccount(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
ruleID := mux.Vars(r)["id"]
|
||||
if len(ruleID) == 0 {
|
||||
http.Error(w, "invalid rule ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rule, err := h.accountManager.GetRule(account.Id, ruleID)
|
||||
if err != nil {
|
||||
http.Error(w, "rule not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, toRuleResponse(account, rule))
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Rules) getRuleAccount(r *http.Request) (*server.Account, error) {
|
||||
jwtClaims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
|
||||
|
||||
account, err := h.accountManager.GetAccountWithAuthorizationClaims(jwtClaims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func toRuleResponse(account *server.Account, rule *server.Rule) *RuleResponse {
|
||||
gr := RuleResponse{
|
||||
ID: rule.ID,
|
||||
Name: rule.Name,
|
||||
}
|
||||
|
||||
switch rule.Flow {
|
||||
case server.TrafficFlowBidirect:
|
||||
gr.Flow = FlowBidirectString
|
||||
default:
|
||||
gr.Flow = "unknown"
|
||||
}
|
||||
|
||||
for _, gid := range rule.Source {
|
||||
if group, ok := account.Groups[gid]; ok {
|
||||
gr.Source = append(gr.Source, RuleGroupResponse{
|
||||
ID: group.ID,
|
||||
Name: group.Name,
|
||||
PeersCount: len(group.Peers),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, gid := range rule.Destination {
|
||||
if group, ok := account.Groups[gid]; ok {
|
||||
gr.Destination = append(gr.Destination, RuleGroupResponse{
|
||||
ID: group.ID,
|
||||
Name: group.Name,
|
||||
PeersCount: len(group.Peers),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &gr
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
package handler
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//Peers is a handler that returns peers of the account
|
||||
@@ -19,21 +18,6 @@ type Peers struct {
|
||||
jwtExtractor jwtclaims.ClaimsExtractor
|
||||
}
|
||||
|
||||
//PeerResponse is a response sent to the client
|
||||
type PeerResponse struct {
|
||||
Name string
|
||||
IP string
|
||||
Connected bool
|
||||
LastSeen time.Time
|
||||
OS string
|
||||
Version string
|
||||
}
|
||||
|
||||
//PeerRequest is a request sent by the client
|
||||
type PeerRequest struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func NewPeers(accountManager server.AccountManager, authAudience string) *Peers {
|
||||
return &Peers{
|
||||
accountManager: accountManager,
|
||||
@@ -42,21 +26,23 @@ func NewPeers(accountManager server.AccountManager, authAudience string) *Peers
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Peers) updatePeer(accountId string, peer *server.Peer, w http.ResponseWriter, r *http.Request) {
|
||||
req := &PeerRequest{}
|
||||
func (h *Peers) updatePeer(account *server.Account, peer *server.Peer, w http.ResponseWriter, r *http.Request) {
|
||||
req := &api.PutApiPeersIdJSONBody{}
|
||||
peerIp := peer.IP
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
peer, err = h.accountManager.RenamePeer(accountId, peer.Key, req.Name)
|
||||
|
||||
update := &server.Peer{Key: peer.Key, SSHEnabled: req.SshEnabled, Name: req.Name}
|
||||
peer, err = h.accountManager.UpdatePeer(account.Id, update)
|
||||
if err != nil {
|
||||
log.Errorf("failed updating peer %s under account %s %v", peerIp, accountId, err)
|
||||
log.Errorf("failed updating peer %s under account %s %v", peerIp, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSONObject(w, toPeerResponse(peer))
|
||||
writeJSONObject(w, toPeerResponse(peer, account))
|
||||
}
|
||||
|
||||
func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -69,19 +55,8 @@ func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseW
|
||||
writeJSONObject(w, "")
|
||||
}
|
||||
|
||||
func (h *Peers) getPeerAccount(r *http.Request) (*server.Account, error) {
|
||||
jwtClaims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
|
||||
|
||||
account, err := h.accountManager.GetAccountWithAuthorizationClaims(jwtClaims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := h.getPeerAccount(r)
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
@@ -105,10 +80,10 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
|
||||
h.deletePeer(account.Id, peer, w, r)
|
||||
return
|
||||
case http.MethodPut:
|
||||
h.updatePeer(account.Id, peer, w, r)
|
||||
h.updatePeer(account, peer, w, r)
|
||||
return
|
||||
case http.MethodGet:
|
||||
writeJSONObject(w, toPeerResponse(peer))
|
||||
writeJSONObject(w, toPeerResponse(peer, account))
|
||||
return
|
||||
|
||||
default:
|
||||
@@ -120,16 +95,16 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
account, err := h.getPeerAccount(r)
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
respBody := []*PeerResponse{}
|
||||
respBody := []*api.Peer{}
|
||||
for _, peer := range account.Peers {
|
||||
respBody = append(respBody, toPeerResponse(peer))
|
||||
respBody = append(respBody, toPeerResponse(peer, account))
|
||||
}
|
||||
writeJSONObject(w, respBody)
|
||||
return
|
||||
@@ -138,13 +113,36 @@ func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func toPeerResponse(peer *server.Peer) *PeerResponse {
|
||||
return &PeerResponse{
|
||||
Name: peer.Name,
|
||||
IP: peer.IP.String(),
|
||||
Connected: peer.Status.Connected,
|
||||
LastSeen: peer.Status.LastSeen,
|
||||
OS: fmt.Sprintf("%s %s", peer.Meta.OS, peer.Meta.Core),
|
||||
Version: peer.Meta.WtVersion,
|
||||
func toPeerResponse(peer *server.Peer, account *server.Account) *api.Peer {
|
||||
var groupsInfo []api.GroupMinimum
|
||||
groupsChecked := make(map[string]struct{})
|
||||
for _, group := range account.Groups {
|
||||
_, ok := groupsChecked[group.ID]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
groupsChecked[group.ID] = struct{}{}
|
||||
for _, pk := range group.Peers {
|
||||
if pk == peer.Key {
|
||||
info := api.GroupMinimum{
|
||||
Id: group.ID,
|
||||
Name: group.Name,
|
||||
PeersCount: len(group.Peers),
|
||||
}
|
||||
groupsInfo = append(groupsInfo, info)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return &api.Peer{
|
||||
Id: peer.IP.String(),
|
||||
Name: peer.Name,
|
||||
Ip: peer.IP.String(),
|
||||
Connected: peer.Status.Connected,
|
||||
LastSeen: peer.Status.LastSeen,
|
||||
Os: fmt.Sprintf("%s %s", peer.Meta.OS, peer.Meta.Core),
|
||||
Version: peer.Meta.WtVersion,
|
||||
Groups: groupsInfo,
|
||||
SshEnabled: peer.SSHEnabled,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package handler
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -98,7 +99,7 @@ func TestGetPeers(t *testing.T) {
|
||||
t.Fatalf("I don't know what I expected; %v", err)
|
||||
}
|
||||
|
||||
respBody := []*PeerResponse{}
|
||||
respBody := []*api.Peer{}
|
||||
err = json.Unmarshal(content, &respBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
@@ -107,8 +108,8 @@ func TestGetPeers(t *testing.T) {
|
||||
got := respBody[0]
|
||||
assert.Equal(t, got.Name, peer.Name)
|
||||
assert.Equal(t, got.Version, peer.Meta.WtVersion)
|
||||
assert.Equal(t, got.IP, peer.IP.String())
|
||||
assert.Equal(t, got.OS, "OS core")
|
||||
assert.Equal(t, got.Ip, peer.IP.String())
|
||||
assert.Equal(t, got.Os, "OS core")
|
||||
})
|
||||
}
|
||||
}
|
||||
403
management/server/http/routes.go
Normal file
403
management/server/http/routes.go
Normal file
@@ -0,0 +1,403 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"net/http"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Routes is the routes handler of the account
|
||||
type Routes struct {
|
||||
jwtExtractor jwtclaims.ClaimsExtractor
|
||||
accountManager server.AccountManager
|
||||
authAudience string
|
||||
}
|
||||
|
||||
// NewRoutes returns a new instance of Routes handler
|
||||
func NewRoutes(accountManager server.AccountManager, authAudience string) *Routes {
|
||||
return &Routes{
|
||||
accountManager: accountManager,
|
||||
authAudience: authAudience,
|
||||
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllRoutesHandler returns the list of routes for the account
|
||||
func (h *Routes) GetAllRoutesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
routes, err := h.accountManager.ListRoutes(account.Id)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
apiRoutes := make([]*api.Route, 0)
|
||||
for _, r := range routes {
|
||||
apiRoutes = append(apiRoutes, toRouteResponse(account, r))
|
||||
}
|
||||
|
||||
writeJSONObject(w, apiRoutes)
|
||||
}
|
||||
|
||||
// CreateRouteHandler handles route creation request
|
||||
func (h *Routes) CreateRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PostApiRoutesJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
peerKey := req.Peer
|
||||
if req.Peer != "" {
|
||||
peer, err := h.accountManager.GetPeerByIP(account.Id, req.Peer)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
peerKey = peer.Key
|
||||
}
|
||||
|
||||
_, newPrefix, err := route.ParseNetwork(req.Network)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("couldn't parse update prefix %s", req.Network), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if utf8.RuneCountInString(req.NetworkId) > route.MaxNetIDChar || req.NetworkId == "" {
|
||||
http.Error(w, fmt.Sprintf("identifier should be between 1 and %d", route.MaxNetIDChar), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
newRoute, err := h.accountManager.CreateRoute(account.Id, newPrefix.String(), peerKey, req.Description, req.NetworkId, req.Masquerade, req.Metric, req.Enabled)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := toRouteResponse(account, newRoute)
|
||||
|
||||
writeJSONObject(w, &resp)
|
||||
}
|
||||
|
||||
// UpdateRouteHandler handles update to a route identified by a given ID
|
||||
func (h *Routes) UpdateRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
routeID := vars["id"]
|
||||
if len(routeID) == 0 {
|
||||
http.Error(w, "invalid route Id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.accountManager.GetRoute(account.Id, routeID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("couldn't find route for ID %s", routeID), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PutApiRoutesIdJSONBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
prefixType, newPrefix, err := route.ParseNetwork(req.Network)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("couldn't parse update prefix %s for route ID %s", req.Network, routeID), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
peerKey := req.Peer
|
||||
if req.Peer != "" {
|
||||
peer, err := h.accountManager.GetPeerByIP(account.Id, req.Peer)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
peerKey = peer.Key
|
||||
}
|
||||
|
||||
if utf8.RuneCountInString(req.NetworkId) > route.MaxNetIDChar || req.NetworkId == "" {
|
||||
http.Error(w, fmt.Sprintf("identifier should be between 1 and %d", route.MaxNetIDChar), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
newRoute := &route.Route{
|
||||
ID: routeID,
|
||||
Network: newPrefix,
|
||||
NetID: req.NetworkId,
|
||||
NetworkType: prefixType,
|
||||
Masquerade: req.Masquerade,
|
||||
Peer: peerKey,
|
||||
Metric: req.Metric,
|
||||
Description: req.Description,
|
||||
Enabled: req.Enabled,
|
||||
}
|
||||
|
||||
err = h.accountManager.SaveRoute(account.Id, newRoute)
|
||||
if err != nil {
|
||||
log.Errorf("failed updating route \"%s\" under account %s %v", routeID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := toRouteResponse(account, newRoute)
|
||||
|
||||
writeJSONObject(w, &resp)
|
||||
}
|
||||
|
||||
// PatchRouteHandler handles patch updates to a route identified by a given ID
|
||||
func (h *Routes) PatchRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
routeID := vars["id"]
|
||||
if len(routeID) == 0 {
|
||||
http.Error(w, "invalid route ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.accountManager.GetRoute(account.Id, routeID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, fmt.Sprintf("couldn't find route ID %s", routeID), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PatchApiRoutesIdJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req) == 0 {
|
||||
http.Error(w, "no patch instruction received", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var operations []server.RouteUpdateOperation
|
||||
|
||||
for _, patch := range req {
|
||||
switch patch.Path {
|
||||
case api.RoutePatchOperationPathNetwork:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Network field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRouteNetwork,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RoutePatchOperationPathDescription:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Description field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRouteDescription,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RoutePatchOperationPathNetworkId:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Network Identifier field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRouteNetworkIdentifier,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RoutePatchOperationPathPeer:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Peer field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(patch.Value) > 1 {
|
||||
http.Error(w, fmt.Sprintf("Value field only accepts 1 value, got %d", len(patch.Value)),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
peerValue := patch.Value
|
||||
if patch.Value[0] != "" {
|
||||
peer, err := h.accountManager.GetPeerByIP(account.Id, patch.Value[0])
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
peerValue = []string{peer.Key}
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRoutePeer,
|
||||
Values: peerValue,
|
||||
})
|
||||
case api.RoutePatchOperationPathMetric:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Metric field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRouteMetric,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RoutePatchOperationPathMasquerade:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Masquerade field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRouteMasquerade,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RoutePatchOperationPathEnabled:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Enabled field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRouteEnabled,
|
||||
Values: patch.Value,
|
||||
})
|
||||
default:
|
||||
http.Error(w, "invalid patch path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
route, err := h.accountManager.UpdateRoute(account.Id, routeID, operations)
|
||||
|
||||
if err != nil {
|
||||
errStatus, ok := status.FromError(err)
|
||||
if ok && errStatus.Code() == codes.Internal {
|
||||
http.Error(w, errStatus.String(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if ok && errStatus.Code() == codes.NotFound {
|
||||
http.Error(w, errStatus.String(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if ok && errStatus.Code() == codes.InvalidArgument {
|
||||
http.Error(w, errStatus.String(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Errorf("failed updating route %s under account %s %v", routeID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := toRouteResponse(account, route)
|
||||
|
||||
writeJSONObject(w, &resp)
|
||||
}
|
||||
|
||||
// DeleteRouteHandler handles route deletion request
|
||||
func (h *Routes) DeleteRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
routeID := mux.Vars(r)["id"]
|
||||
if len(routeID) == 0 {
|
||||
http.Error(w, "invalid route ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.accountManager.DeleteRoute(account.Id, routeID)
|
||||
if err != nil {
|
||||
log.Errorf("failed delete route %s under account %s %v", routeID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, "")
|
||||
}
|
||||
|
||||
// GetRouteHandler handles a route Get request identified by ID
|
||||
func (h *Routes) GetRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
routeID := mux.Vars(r)["id"]
|
||||
if len(routeID) == 0 {
|
||||
http.Error(w, "invalid route ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
foundRoute, err := h.accountManager.GetRoute(account.Id, routeID)
|
||||
if err != nil {
|
||||
http.Error(w, "route not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, toRouteResponse(account, foundRoute))
|
||||
}
|
||||
|
||||
func toRouteResponse(account *server.Account, serverRoute *route.Route) *api.Route {
|
||||
var peerIP string
|
||||
if serverRoute.Peer != "" {
|
||||
peer, found := account.Peers[serverRoute.Peer]
|
||||
if !found {
|
||||
panic("peer ID not found")
|
||||
}
|
||||
peerIP = peer.IP.String()
|
||||
}
|
||||
|
||||
return &api.Route{
|
||||
Id: serverRoute.ID,
|
||||
Description: serverRoute.Description,
|
||||
NetworkId: serverRoute.NetID,
|
||||
Enabled: serverRoute.Enabled,
|
||||
Peer: peerIP,
|
||||
Network: serverRoute.Network.String(),
|
||||
NetworkType: serverRoute.NetworkType.String(),
|
||||
Masquerade: serverRoute.Masquerade,
|
||||
Metric: serverRoute.Metric,
|
||||
}
|
||||
}
|
||||
362
management/server/http/routes_test.go
Normal file
362
management/server/http/routes_test.go
Normal file
@@ -0,0 +1,362 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||
)
|
||||
|
||||
const (
|
||||
existingRouteID = "existingRouteID"
|
||||
notFoundRouteID = "notFoundRouteID"
|
||||
existingPeerID = "100.64.0.100"
|
||||
notFoundPeerID = "100.64.0.200"
|
||||
existingPeerKey = "existingPeerKey"
|
||||
testAccountID = "test_id"
|
||||
)
|
||||
|
||||
var baseExistingRoute = &route.Route{
|
||||
ID: existingRouteID,
|
||||
Description: "base route",
|
||||
NetID: "awesomeNet",
|
||||
Network: netip.MustParsePrefix("192.168.0.0/24"),
|
||||
NetworkType: route.IPv4Network,
|
||||
Metric: 9999,
|
||||
Masquerade: false,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
var testingAccount = &server.Account{
|
||||
Id: testAccountID,
|
||||
Domain: "hotmail.com",
|
||||
Peers: map[string]*server.Peer{
|
||||
existingPeerKey: {
|
||||
Key: existingPeerID,
|
||||
IP: netip.MustParseAddr(existingPeerID).AsSlice(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func initRoutesTestData() *Routes {
|
||||
return &Routes{
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
GetRouteFunc: func(_, routeID string) (*route.Route, error) {
|
||||
if routeID == existingRouteID {
|
||||
return baseExistingRoute, nil
|
||||
}
|
||||
return nil, status.Errorf(codes.NotFound, "route with ID %s not found", routeID)
|
||||
},
|
||||
CreateRouteFunc: func(accountID string, network, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error) {
|
||||
networkType, p, _ := route.ParseNetwork(network)
|
||||
return &route.Route{
|
||||
ID: existingRouteID,
|
||||
NetID: netID,
|
||||
Peer: peer,
|
||||
Network: p,
|
||||
NetworkType: networkType,
|
||||
Description: description,
|
||||
Masquerade: masquerade,
|
||||
Enabled: enabled,
|
||||
}, nil
|
||||
},
|
||||
SaveRouteFunc: func(_ string, _ *route.Route) error {
|
||||
return nil
|
||||
},
|
||||
DeleteRouteFunc: func(_ string, _ string) error {
|
||||
return nil
|
||||
},
|
||||
GetPeerByIPFunc: func(_ string, peerIP string) (*server.Peer, error) {
|
||||
if peerIP != existingPeerID {
|
||||
return nil, status.Errorf(codes.NotFound, "Peer with ID %s not found", peerIP)
|
||||
}
|
||||
return &server.Peer{
|
||||
Key: existingPeerKey,
|
||||
IP: netip.MustParseAddr(existingPeerID).AsSlice(),
|
||||
}, nil
|
||||
},
|
||||
UpdateRouteFunc: func(_ string, routeID string, operations []server.RouteUpdateOperation) (*route.Route, error) {
|
||||
routeToUpdate := baseExistingRoute
|
||||
if routeID != routeToUpdate.ID {
|
||||
return nil, status.Errorf(codes.NotFound, "route %s no longer exists", routeID)
|
||||
}
|
||||
for _, operation := range operations {
|
||||
switch operation.Type {
|
||||
case server.UpdateRouteNetwork:
|
||||
routeToUpdate.NetworkType, routeToUpdate.Network, _ = route.ParseNetwork(operation.Values[0])
|
||||
case server.UpdateRouteDescription:
|
||||
routeToUpdate.Description = operation.Values[0]
|
||||
case server.UpdateRouteNetworkIdentifier:
|
||||
routeToUpdate.NetID = operation.Values[0]
|
||||
case server.UpdateRoutePeer:
|
||||
routeToUpdate.Peer = operation.Values[0]
|
||||
case server.UpdateRouteMetric:
|
||||
routeToUpdate.Metric, _ = strconv.Atoi(operation.Values[0])
|
||||
case server.UpdateRouteMasquerade:
|
||||
routeToUpdate.Masquerade, _ = strconv.ParseBool(operation.Values[0])
|
||||
case server.UpdateRouteEnabled:
|
||||
routeToUpdate.Enabled, _ = strconv.ParseBool(operation.Values[0])
|
||||
default:
|
||||
return nil, fmt.Errorf("no operation")
|
||||
}
|
||||
}
|
||||
return routeToUpdate, nil
|
||||
},
|
||||
GetAccountWithAuthorizationClaimsFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
return testingAccount, nil
|
||||
},
|
||||
},
|
||||
authAudience: "",
|
||||
jwtExtractor: jwtclaims.ClaimsExtractor{
|
||||
ExtractClaimsFromRequestContext: func(r *http.Request, authAudiance string) jwtclaims.AuthorizationClaims {
|
||||
return jwtclaims.AuthorizationClaims{
|
||||
UserId: "test_user",
|
||||
Domain: "hotmail.com",
|
||||
AccountId: testAccountID,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutesHandlers(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
expectedBody bool
|
||||
expectedRoute *api.Route
|
||||
requestType string
|
||||
requestPath string
|
||||
requestBody io.Reader
|
||||
}{
|
||||
{
|
||||
name: "Get Existing Route",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRoute: toRouteResponse(testingAccount, baseExistingRoute),
|
||||
},
|
||||
{
|
||||
name: "Get Not Existing Route",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/api/rules/" + notFoundRouteID,
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "Delete Existing Route",
|
||||
requestType: http.MethodDelete,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Delete Not Existing Route",
|
||||
requestType: http.MethodDelete,
|
||||
requestPath: "/api/rules/" + notFoundRouteID,
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "POST OK",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/routes",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID))),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRoute: &api.Route{
|
||||
Id: existingRouteID,
|
||||
Description: "Post",
|
||||
NetworkId: "awesomeNet",
|
||||
Network: "192.168.0.0/16",
|
||||
Peer: existingPeerID,
|
||||
NetworkType: route.IPv4NetworkString,
|
||||
Masquerade: false,
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "POST Not Found Peer",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/routes",
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", notFoundPeerID)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "POST Not Invalid Network Identifier",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/routes",
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"12345678901234567890qwertyuiopqwertyuiop1\",\"Peer\":\"%s\"}", existingPeerID)),
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "POST Invalid Network",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/routes",
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/34\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID)),
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "PUT OK",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRoute: &api.Route{
|
||||
Id: existingRouteID,
|
||||
Description: "Post",
|
||||
NetworkId: "awesomeNet",
|
||||
Network: "192.168.0.0/16",
|
||||
Peer: existingPeerID,
|
||||
NetworkType: route.IPv4NetworkString,
|
||||
Masquerade: false,
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PUT Not Found Route",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/routes/" + notFoundRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID)),
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "PUT Not Found Peer",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", notFoundPeerID)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "PUT Invalid Network Identifier",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"12345678901234567890qwertyuiopqwertyuiop1\",\"Peer\":\"%s\"}", existingPeerID)),
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "PUT Invalid Network",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/34\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID)),
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "PATCH Description OK",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString("[{\"op\":\"replace\",\"path\":\"description\",\"value\":[\"NewDesc\"]}]"),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRoute: &api.Route{
|
||||
Id: existingRouteID,
|
||||
Description: "NewDesc",
|
||||
NetworkId: "awesomeNet",
|
||||
Network: baseExistingRoute.Network.String(),
|
||||
NetworkType: route.IPv4NetworkString,
|
||||
Masquerade: baseExistingRoute.Masquerade,
|
||||
Enabled: baseExistingRoute.Enabled,
|
||||
Metric: baseExistingRoute.Metric,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PATCH Peer OK",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("[{\"op\":\"replace\",\"path\":\"peer\",\"value\":[\"%s\"]}]", existingPeerID)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRoute: &api.Route{
|
||||
Id: existingRouteID,
|
||||
Description: "NewDesc",
|
||||
NetworkId: "awesomeNet",
|
||||
Network: baseExistingRoute.Network.String(),
|
||||
NetworkType: route.IPv4NetworkString,
|
||||
Peer: existingPeerID,
|
||||
Masquerade: baseExistingRoute.Masquerade,
|
||||
Enabled: baseExistingRoute.Enabled,
|
||||
Metric: baseExistingRoute.Metric,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PATCH Not Found Peer",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("[{\"op\":\"replace\",\"path\":\"peer\",\"value\":[\"%s\"]}]", notFoundPeerID)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "PATCH Not Found Route",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/routes/" + notFoundRouteID,
|
||||
requestBody: bytes.NewBufferString("[{\"op\":\"replace\",\"path\":\"network\",\"value\":[\"192.168.0.0/34\"]}]"),
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectedBody: false,
|
||||
},
|
||||
}
|
||||
|
||||
p := initRoutesTestData()
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/api/routes/{id}", p.GetRouteHandler).Methods("GET")
|
||||
router.HandleFunc("/api/routes/{id}", p.DeleteRouteHandler).Methods("DELETE")
|
||||
router.HandleFunc("/api/routes", p.CreateRouteHandler).Methods("POST")
|
||||
router.HandleFunc("/api/routes/{id}", p.UpdateRouteHandler).Methods("PUT")
|
||||
router.HandleFunc("/api/routes/{id}", p.PatchRouteHandler).Methods("PATCH")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
content, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("I don't know what I expected; %v", err)
|
||||
}
|
||||
|
||||
if status := recorder.Code; status != tc.expectedStatus {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v, content: %s",
|
||||
status, tc.expectedStatus, string(content))
|
||||
return
|
||||
}
|
||||
|
||||
if !tc.expectedBody {
|
||||
return
|
||||
}
|
||||
|
||||
got := &api.Route{}
|
||||
if err = json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
assert.Equal(t, got, tc.expectedRoute)
|
||||
})
|
||||
}
|
||||
}
|
||||
437
management/server/http/rules.go
Normal file
437
management/server/http/rules.go
Normal file
@@ -0,0 +1,437 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Rules is a handler that returns rules of the account
|
||||
type Rules struct {
|
||||
jwtExtractor jwtclaims.ClaimsExtractor
|
||||
accountManager server.AccountManager
|
||||
authAudience string
|
||||
}
|
||||
|
||||
func NewRules(accountManager server.AccountManager, authAudience string) *Rules {
|
||||
return &Rules{
|
||||
accountManager: accountManager,
|
||||
authAudience: authAudience,
|
||||
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllRulesHandler list for the account
|
||||
func (h *Rules) GetAllRulesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rules := []*api.Rule{}
|
||||
for _, r := range account.Rules {
|
||||
rules = append(rules, toRuleResponse(account, r))
|
||||
}
|
||||
|
||||
writeJSONObject(w, rules)
|
||||
}
|
||||
|
||||
// UpdateRuleHandler handles update to a rule identified by a given ID
|
||||
func (h *Rules) UpdateRuleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
ruleID := vars["id"]
|
||||
if len(ruleID) == 0 {
|
||||
http.Error(w, "invalid rule Id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, ok := account.Rules[ruleID]
|
||||
if !ok {
|
||||
http.Error(w, fmt.Sprintf("couldn't find rule id %s", ruleID), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PutApiRulesIdJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
http.Error(w, "Rule name shouldn't be empty", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
var reqSources []string
|
||||
if req.Sources != nil {
|
||||
reqSources = *req.Sources
|
||||
}
|
||||
|
||||
var reqDestinations []string
|
||||
if req.Destinations != nil {
|
||||
reqDestinations = *req.Destinations
|
||||
}
|
||||
|
||||
rule := server.Rule{
|
||||
ID: ruleID,
|
||||
Name: req.Name,
|
||||
Source: reqSources,
|
||||
Destination: reqDestinations,
|
||||
Disabled: req.Disabled,
|
||||
Description: req.Description,
|
||||
}
|
||||
|
||||
switch req.Flow {
|
||||
case server.TrafficFlowBidirectString:
|
||||
rule.Flow = server.TrafficFlowBidirect
|
||||
default:
|
||||
http.Error(w, "unknown flow type", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.accountManager.SaveRule(account.Id, &rule); err != nil {
|
||||
log.Errorf("failed updating rule \"%s\" under account %s %v", ruleID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := toRuleResponse(account, &rule)
|
||||
|
||||
writeJSONObject(w, &resp)
|
||||
}
|
||||
|
||||
// PatchRuleHandler handles patch updates to a rule identified by a given ID
|
||||
func (h *Rules) PatchRuleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
ruleID := vars["id"]
|
||||
if len(ruleID) == 0 {
|
||||
http.Error(w, "invalid rule Id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, ok := account.Rules[ruleID]
|
||||
if !ok {
|
||||
http.Error(w, fmt.Sprintf("couldn't find rule id %s", ruleID), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PatchApiRulesIdJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req) == 0 {
|
||||
http.Error(w, "no patch instruction received", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var operations []server.RuleUpdateOperation
|
||||
|
||||
for _, patch := range req {
|
||||
switch patch.Path {
|
||||
case api.RulePatchOperationPathName:
|
||||
if patch.Op != api.RulePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Name field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(patch.Value) == 0 || patch.Value[0] == "" {
|
||||
http.Error(w, "Rule name shouldn't be empty", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.UpdateRuleName,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationPathDescription:
|
||||
if patch.Op != api.RulePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Description field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.UpdateRuleDescription,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationPathFlow:
|
||||
if patch.Op != api.RulePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Flow field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.UpdateRuleFlow,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationPathDisabled:
|
||||
if patch.Op != api.RulePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Disabled field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.UpdateRuleStatus,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationPathSources:
|
||||
switch patch.Op {
|
||||
case api.RulePatchOperationOpReplace:
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.UpdateSourceGroups,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationOpRemove:
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.RemoveGroupsFromSource,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationOpAdd:
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.InsertGroupsToSource,
|
||||
Values: patch.Value,
|
||||
})
|
||||
default:
|
||||
http.Error(w, "invalid operation, \"%s\", for Source field", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
case api.RulePatchOperationPathDestinations:
|
||||
switch patch.Op {
|
||||
case api.RulePatchOperationOpReplace:
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.UpdateDestinationGroups,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationOpRemove:
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.RemoveGroupsFromDestination,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationOpAdd:
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.InsertGroupsToDestination,
|
||||
Values: patch.Value,
|
||||
})
|
||||
default:
|
||||
http.Error(w, "invalid operation, \"%s\", for Destination field", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
default:
|
||||
http.Error(w, "invalid patch path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rule, err := h.accountManager.UpdateRule(account.Id, ruleID, operations)
|
||||
|
||||
if err != nil {
|
||||
errStatus, ok := status.FromError(err)
|
||||
if ok && errStatus.Code() == codes.Internal {
|
||||
http.Error(w, errStatus.String(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if ok && errStatus.Code() == codes.NotFound {
|
||||
http.Error(w, errStatus.String(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if ok && errStatus.Code() == codes.InvalidArgument {
|
||||
http.Error(w, errStatus.String(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Errorf("failed updating rule %s under account %s %v", ruleID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := toRuleResponse(account, rule)
|
||||
|
||||
writeJSONObject(w, &resp)
|
||||
}
|
||||
|
||||
// CreateRuleHandler handles rule creation request
|
||||
func (h *Rules) CreateRuleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PostApiRulesJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
http.Error(w, "Rule name shouldn't be empty", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
var reqSources []string
|
||||
if req.Sources != nil {
|
||||
reqSources = *req.Sources
|
||||
}
|
||||
|
||||
var reqDestinations []string
|
||||
if req.Destinations != nil {
|
||||
reqDestinations = *req.Destinations
|
||||
}
|
||||
|
||||
rule := server.Rule{
|
||||
ID: xid.New().String(),
|
||||
Name: req.Name,
|
||||
Source: reqSources,
|
||||
Destination: reqDestinations,
|
||||
Disabled: req.Disabled,
|
||||
Description: req.Description,
|
||||
}
|
||||
|
||||
switch req.Flow {
|
||||
case server.TrafficFlowBidirectString:
|
||||
rule.Flow = server.TrafficFlowBidirect
|
||||
default:
|
||||
http.Error(w, "unknown flow type", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.accountManager.SaveRule(account.Id, &rule); err != nil {
|
||||
log.Errorf("failed creating rule \"%s\" under account %s %v", req.Name, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := toRuleResponse(account, &rule)
|
||||
|
||||
writeJSONObject(w, &resp)
|
||||
}
|
||||
|
||||
// DeleteRuleHandler handles rule deletion request
|
||||
func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
aID := account.Id
|
||||
|
||||
rID := mux.Vars(r)["id"]
|
||||
if len(rID) == 0 {
|
||||
http.Error(w, "invalid rule ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.accountManager.DeleteRule(aID, rID); err != nil {
|
||||
log.Errorf("failed delete rule %s under account %s %v", rID, aID, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, "")
|
||||
}
|
||||
|
||||
// GetRuleHandler handles a group Get request identified by ID
|
||||
func (h *Rules) GetRuleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
ruleID := mux.Vars(r)["id"]
|
||||
if len(ruleID) == 0 {
|
||||
http.Error(w, "invalid rule ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rule, err := h.accountManager.GetRule(account.Id, ruleID)
|
||||
if err != nil {
|
||||
http.Error(w, "rule not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, toRuleResponse(account, rule))
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func toRuleResponse(account *server.Account, rule *server.Rule) *api.Rule {
|
||||
cache := make(map[string]api.GroupMinimum)
|
||||
gr := api.Rule{
|
||||
Id: rule.ID,
|
||||
Name: rule.Name,
|
||||
Description: rule.Description,
|
||||
Disabled: rule.Disabled,
|
||||
}
|
||||
|
||||
switch rule.Flow {
|
||||
case server.TrafficFlowBidirect:
|
||||
gr.Flow = server.TrafficFlowBidirectString
|
||||
default:
|
||||
gr.Flow = "unknown"
|
||||
}
|
||||
|
||||
for _, gid := range rule.Source {
|
||||
_, ok := cache[gid]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if group, ok := account.Groups[gid]; ok {
|
||||
minimum := api.GroupMinimum{
|
||||
Id: group.ID,
|
||||
Name: group.Name,
|
||||
PeersCount: len(group.Peers),
|
||||
}
|
||||
|
||||
gr.Sources = append(gr.Sources, minimum)
|
||||
cache[gid] = minimum
|
||||
}
|
||||
}
|
||||
|
||||
for _, gid := range rule.Destination {
|
||||
cachedMinimum, ok := cache[gid]
|
||||
if ok {
|
||||
gr.Destinations = append(gr.Destinations, cachedMinimum)
|
||||
continue
|
||||
}
|
||||
if group, ok := account.Groups[gid]; ok {
|
||||
minimum := api.GroupMinimum{
|
||||
Id: group.ID,
|
||||
Name: group.Name,
|
||||
PeersCount: len(group.Peers),
|
||||
}
|
||||
gr.Destinations = append(gr.Destinations, minimum)
|
||||
cache[gid] = minimum
|
||||
}
|
||||
}
|
||||
|
||||
return &gr
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package handler
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -39,10 +40,41 @@ func initRulesTestData(rules ...*server.Rule) *Rules {
|
||||
Flow: server.TrafficFlowBidirect,
|
||||
}, nil
|
||||
},
|
||||
UpdateRuleFunc: func(_ string, ruleID string, operations []server.RuleUpdateOperation) (*server.Rule, error) {
|
||||
var rule server.Rule
|
||||
rule.ID = ruleID
|
||||
for _, operation := range operations {
|
||||
switch operation.Type {
|
||||
case server.UpdateRuleName:
|
||||
rule.Name = operation.Values[0]
|
||||
case server.UpdateRuleDescription:
|
||||
rule.Description = operation.Values[0]
|
||||
case server.UpdateRuleFlow:
|
||||
if server.TrafficFlowBidirectString == operation.Values[0] {
|
||||
rule.Flow = server.TrafficFlowBidirect
|
||||
} else {
|
||||
rule.Flow = 100
|
||||
}
|
||||
case server.UpdateSourceGroups, server.InsertGroupsToSource:
|
||||
rule.Source = operation.Values
|
||||
case server.UpdateDestinationGroups, server.InsertGroupsToDestination:
|
||||
rule.Destination = operation.Values
|
||||
case server.RemoveGroupsFromSource, server.RemoveGroupsFromDestination:
|
||||
default:
|
||||
return nil, fmt.Errorf("no operation")
|
||||
}
|
||||
}
|
||||
return &rule, nil
|
||||
},
|
||||
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
return &server.Account{
|
||||
Id: claims.AccountId,
|
||||
Domain: "hotmail.com",
|
||||
Rules: map[string]*server.Rule{"id-existed": &server.Rule{ID: "id-existed"}},
|
||||
Groups: map[string]*server.Group{
|
||||
"F": &server.Group{ID: "F"},
|
||||
"G": &server.Group{ID: "G"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
@@ -117,52 +149,118 @@ func TestRulesGetRule(t *testing.T) {
|
||||
t.Fatalf("I don't know what I expected; %v", err)
|
||||
}
|
||||
|
||||
var got RuleResponse
|
||||
var got api.Rule
|
||||
if err = json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, got.ID, rule.ID)
|
||||
assert.Equal(t, got.Id, rule.ID)
|
||||
assert.Equal(t, got.Name, rule.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRulesSaveRule(t *testing.T) {
|
||||
func TestRulesWriteRule(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
expectedBody bool
|
||||
expectedRule *server.Rule
|
||||
expectedRule *api.Rule
|
||||
requestType string
|
||||
requestPath string
|
||||
requestBody io.Reader
|
||||
}{
|
||||
{
|
||||
name: "SaveRule POST OK",
|
||||
name: "WriteRule POST OK",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/rules",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`{"Name":"Default POSTed Rule","Flow":"bidirect"}`)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRule: &server.Rule{
|
||||
ID: "id-was-set",
|
||||
expectedRule: &api.Rule{
|
||||
Id: "id-was-set",
|
||||
Name: "Default POSTed Rule",
|
||||
Flow: server.TrafficFlowBidirect,
|
||||
Flow: server.TrafficFlowBidirectString,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SaveRule PUT OK",
|
||||
requestType: http.MethodPut,
|
||||
name: "WriteRule POST Invalid Name",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/rules",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`{"ID":"id-existed","Name":"Default POSTed Rule","Flow":"bidirect"}`)),
|
||||
[]byte(`{"Name":"","Flow":"bidirect"}`)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "WriteRule PUT OK",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/rules/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`{"Name":"Default POSTed Rule","Flow":"bidirect"}`)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedRule: &server.Rule{
|
||||
ID: "id-existed",
|
||||
expectedBody: true,
|
||||
expectedRule: &api.Rule{
|
||||
Id: "id-existed",
|
||||
Name: "Default POSTed Rule",
|
||||
Flow: server.TrafficFlowBidirect,
|
||||
Flow: server.TrafficFlowBidirectString,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "WriteRule PUT Invalid Name",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/rules/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`{"Name":"","Flow":"bidirect"}`)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "Write Rule PATCH Name OK",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/rules/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`[{"op":"replace","path":"name","value":["Default POSTed Rule"]}]`)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRule: &api.Rule{
|
||||
Id: "id-existed",
|
||||
Name: "Default POSTed Rule",
|
||||
Flow: server.TrafficFlowBidirectString,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Write Rule PATCH Invalid Name OP",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/rules/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`[{"op":"insert","path":"name","value":[""]}]`)),
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Write Rule PATCH Invalid Name",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/rules/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`[{"op":"replace","path":"name","value":[]}]`)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Write Rule PATCH Sources OK",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/rules/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`[{"op":"replace","path":"sources","value":["G","F"]}]`)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRule: &api.Rule{
|
||||
Id: "id-existed",
|
||||
Flow: server.TrafficFlowBidirectString,
|
||||
Sources: []api.GroupMinimum{
|
||||
{Id: "G"},
|
||||
{Id: "F"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -175,7 +273,9 @@ func TestRulesSaveRule(t *testing.T) {
|
||||
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/api/rules", p.CreateOrUpdateRuleHandler).Methods("PUT", "POST")
|
||||
router.HandleFunc("/api/rules", p.CreateRuleHandler).Methods("POST")
|
||||
router.HandleFunc("/api/rules/{id}", p.UpdateRuleHandler).Methods("PUT")
|
||||
router.HandleFunc("/api/rules/{id}", p.PatchRuleHandler).Methods("PATCH")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
@@ -196,16 +296,13 @@ func TestRulesSaveRule(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
got := &RuleRequest{}
|
||||
got := &api.Rule{}
|
||||
if err = json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
if tc.requestType != http.MethodPost {
|
||||
assert.Equal(t, got.ID, tc.expectedRule.ID)
|
||||
}
|
||||
assert.Equal(t, got.Name, tc.expectedRule.Name)
|
||||
assert.Equal(t, got.Flow, "bidirect")
|
||||
assert.Equal(t, got, tc.expectedRule)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
s "github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http/handler"
|
||||
"github.com/netbirdio/netbird/management/server/http/middleware"
|
||||
"github.com/rs/cors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
server *http.Server
|
||||
config *s.HttpServerConfig
|
||||
certManager *autocert.Manager
|
||||
tlsConfig *tls.Config
|
||||
accountManager s.AccountManager
|
||||
}
|
||||
|
||||
// NewHttpsServer creates a new HTTPs server (with HTTPS support) and a certManager that is responsible for generating and renewing Let's Encrypt certificate
|
||||
// The listening address will be :443 no matter what was specified in s.HttpServerConfig.Address
|
||||
func NewHttpsServer(
|
||||
config *s.HttpServerConfig,
|
||||
certManager *autocert.Manager,
|
||||
accountManager s.AccountManager,
|
||||
) *Server {
|
||||
server := &http.Server{
|
||||
Addr: config.Address,
|
||||
WriteTimeout: time.Second * 15,
|
||||
ReadTimeout: time.Second * 15,
|
||||
IdleTimeout: time.Second * 60,
|
||||
}
|
||||
return &Server{
|
||||
server: server,
|
||||
config: config,
|
||||
certManager: certManager,
|
||||
accountManager: accountManager,
|
||||
}
|
||||
}
|
||||
|
||||
// NewHttpsServerWithTLSConfig creates a new HTTPs server with a provided tls.Config.
|
||||
// Usually used when you already have a certificate
|
||||
func NewHttpsServerWithTLSConfig(
|
||||
config *s.HttpServerConfig,
|
||||
tlsConfig *tls.Config,
|
||||
accountManager s.AccountManager,
|
||||
) *Server {
|
||||
server := &http.Server{
|
||||
Addr: config.Address,
|
||||
WriteTimeout: time.Second * 15,
|
||||
ReadTimeout: time.Second * 15,
|
||||
IdleTimeout: time.Second * 60,
|
||||
}
|
||||
return &Server{
|
||||
server: server,
|
||||
config: config,
|
||||
tlsConfig: tlsConfig,
|
||||
accountManager: accountManager,
|
||||
}
|
||||
}
|
||||
|
||||
// NewHttpServer creates a new HTTP server (without HTTPS)
|
||||
func NewHttpServer(config *s.HttpServerConfig, accountManager s.AccountManager) *Server {
|
||||
return NewHttpsServer(config, nil, accountManager)
|
||||
}
|
||||
|
||||
// Stop stops the http server
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
err := s.server.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start defines http handlers and starts the http server. Blocks until server is shutdown.
|
||||
func (s *Server) Start() error {
|
||||
jwtMiddleware, err := middleware.NewJwtMiddleware(
|
||||
s.config.AuthIssuer,
|
||||
s.config.AuthAudience,
|
||||
s.config.AuthKeysLocation,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
corsMiddleware := cors.AllowAll()
|
||||
|
||||
acMiddleware := middleware.NewAccessControll(
|
||||
s.config.AuthAudience,
|
||||
s.accountManager.IsUserAdmin)
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.Use(jwtMiddleware.Handler, corsMiddleware.Handler, acMiddleware.Handler)
|
||||
|
||||
groupsHandler := handler.NewGroups(s.accountManager, s.config.AuthAudience)
|
||||
rulesHandler := handler.NewRules(s.accountManager, s.config.AuthAudience)
|
||||
peersHandler := handler.NewPeers(s.accountManager, s.config.AuthAudience)
|
||||
keysHandler := handler.NewSetupKeysHandler(s.accountManager, s.config.AuthAudience)
|
||||
r.HandleFunc("/api/peers", peersHandler.GetPeers).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/peers/{id}", peersHandler.HandlePeer).
|
||||
Methods("GET", "PUT", "DELETE", "OPTIONS")
|
||||
|
||||
userHandler := handler.NewUserHandler(s.accountManager, s.config.AuthAudience)
|
||||
r.HandleFunc("/api/users", userHandler.GetUsers).Methods("GET", "OPTIONS")
|
||||
|
||||
r.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("GET", "POST", "OPTIONS")
|
||||
r.HandleFunc("/api/setup-keys/{id}", keysHandler.HandleKey).Methods("GET", "PUT", "OPTIONS")
|
||||
|
||||
r.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/setup-keys/{id}", keysHandler.HandleKey).
|
||||
Methods("GET", "PUT", "DELETE", "OPTIONS")
|
||||
|
||||
r.HandleFunc("/api/rules", rulesHandler.GetAllRulesHandler).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/rules", rulesHandler.CreateOrUpdateRuleHandler).
|
||||
Methods("POST", "PUT", "OPTIONS")
|
||||
r.HandleFunc("/api/rules/{id}", rulesHandler.GetRuleHandler).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/rules/{id}", rulesHandler.DeleteRuleHandler).Methods("DELETE", "OPTIONS")
|
||||
|
||||
r.HandleFunc("/api/groups", groupsHandler.GetAllGroupsHandler).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/groups", groupsHandler.CreateOrUpdateGroupHandler).
|
||||
Methods("POST", "PUT", "OPTIONS")
|
||||
r.HandleFunc("/api/groups/{id}", groupsHandler.GetGroupHandler).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/groups/{id}", groupsHandler.DeleteGroupHandler).Methods("DELETE", "OPTIONS")
|
||||
http.Handle("/", r)
|
||||
|
||||
if s.certManager != nil {
|
||||
// if HTTPS is enabled we reuse the listener from the cert manager
|
||||
listener := s.certManager.Listener()
|
||||
log.Infof(
|
||||
"HTTPs server listening on %s with Let's Encrypt autocert configured",
|
||||
listener.Addr(),
|
||||
)
|
||||
if err = http.Serve(listener, s.certManager.HTTPHandler(r)); err != nil {
|
||||
log.Errorf("failed to serve https server: %v", err)
|
||||
return err
|
||||
}
|
||||
} else if s.tlsConfig != nil {
|
||||
listener, err := tls.Listen("tcp", s.config.Address, s.tlsConfig)
|
||||
if err != nil {
|
||||
log.Errorf("failed to serve https server: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Infof("HTTPs server listening on %s", listener.Addr())
|
||||
|
||||
if err = http.Serve(listener, r); err != nil {
|
||||
log.Errorf("failed to serve https server: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Infof("HTTP server listening on %s", s.server.Addr)
|
||||
if err = s.server.ListenAndServe(); err != nil {
|
||||
log.Errorf("failed to serve http server: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user