Compare commits

..

2 Commits

Author SHA1 Message Date
braginini
ab579f5de0 chore: [management] - fix golint 2022-01-01 14:53:49 +01:00
braginini
09eeb71af2 chore: [management] - replace proactive peer updates with periodic updates 2022-01-01 14:47:57 +01:00
397 changed files with 6463 additions and 58830 deletions

View File

@@ -1,30 +0,0 @@
---
name: Bug/Issue report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the problem**
A clear and concise description of what the problem is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**NetBird status -d output:**
If applicable, add the output of the `netbird status -d` command
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,11 +0,0 @@
## Describe your changes
## Issue ticket number and link
### Checklist
- [ ] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] Extended the README / documentation, if necessary

View File

@@ -1,36 +0,0 @@
name: Test Code Darwin
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
jobs:
test:
runs-on: macos-latest
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.19.x
- name: Checkout code
uses: actions/checkout@v2
- name: Cache Go modules
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: macos-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
macos-go-
- name: Install modules
run: go mod tidy
- name: Test
run: go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...

View File

@@ -1,96 +0,0 @@
name: Test Code Linux
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
jobs:
test:
strategy:
matrix:
arch: ['386','amd64']
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.19.x
- name: Cache Go modules
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib
- name: Install modules
run: go mod tidy
- name: Test
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
test_client_on_docker:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.19.x
- name: Cache Go modules
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
- name: Install modules
run: go mod tidy
- name: Generate Iface Test bin
run: go test -c -o iface-testing.bin ./iface/
- name: Generate RouteManager Test bin
run: go test -c -o routemanager-testing.bin ./client/internal/routemanager/...
- name: Generate Engine Test bin
run: go test -c -o engine-testing.bin ./client/internal
- name: Generate Peer Test bin
run: go test -c -o peer-testing.bin ./client/internal/peer/...
- run: chmod +x *testing.bin
- name: Run Iface tests in docker
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/iface --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/iface-testing.bin -test.timeout 5m -test.parallel 1
- name: Run RouteManager tests in docker
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/routemanager-testing.bin -test.timeout 5m -test.parallel 1
- name: Run Engine tests in docker
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1
- name: Run Peer tests in docker
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/peer --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/peer-testing.bin -test.timeout 5m -test.parallel 1

View File

@@ -1,50 +0,0 @@
name: Test Code Windows
on:
push:
branches:
- main
pull_request:
env:
downloadPath: '${{ github.workspace }}\temp'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
jobs:
test:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
id: go
with:
go-version: 1.19.x
- name: Download wintun
uses: carlosperate/download-file-action@v2
id: download-wintun
with:
file-url: https://www.wintun.net/builds/wintun-0.14.1.zip
file-name: wintun.zip
location: ${{ env.downloadPath }}
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
- name: Decompressing wintun files
run: tar -zvxf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
- run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\'
- run: choco install -y sysinternals
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=C:\Users\runneradmin\go\pkg\mod
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=C:\Users\runneradmin\AppData\Local\go-build
- name: test
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -timeout 5m -p 1 ./... > test-out.txt 2>&1"
- name: test output
if: ${{ always() }}
run: Get-Content test-out.txt

59
.github/workflows/golang-test.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
on:
push:
branches:
- main
pull_request:
name: Test
jobs:
test:
strategy:
matrix:
go-version: [1.17.x]
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Test
run: GOBIN=$(which go) && sudo --preserve-env=GOROOT $GOBIN test -p 1 ./...
test_build:
strategy:
matrix:
os: [ windows, linux, darwin ]
go-version: [1.17.x]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Cache Go modules
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Install modules
run: go mod tidy
- name: run build client
run: GOOS=${{ matrix.os }} go build .
working-directory: client
- name: run build management
run: GOOS=${{ matrix.os }} go build .
working-directory: management
- name: run build signal
run: GOOS=${{ matrix.os }} go build .
working-directory: signal

View File

@@ -1,21 +1,16 @@
name: golangci-lint
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
on:
push:
branches:
- main
pull_request:
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.19.x
- name: Install dependencies
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
args: --timeout=6m

View File

@@ -1,60 +0,0 @@
name: Test installation Darwin
on:
push:
branches:
- main
pull_request:
paths:
- "release_files/install.sh"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
jobs:
install-cli-only:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Rename brew package
if: ${{ matrix.check_bin_install }}
run: mv /opt/homebrew/bin/brew /opt/homebrew/bin/brew.bak
- name: Run install script
run: |
sh ./release_files/install.sh
env:
SKIP_UI_APP: true
- name: Run tests
run: |
if ! command -v netbird &> /dev/null; then
echo "Error: netbird is not installed"
exit 1
fi
install-all:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Rename brew package
if: ${{ matrix.check_bin_install }}
run: mv /opt/homebrew/bin/brew /opt/homebrew/bin/brew.bak
- name: Run install script
run: |
sh ./release_files/install.sh
- name: Run tests
run: |
if ! command -v netbird &> /dev/null; then
echo "Error: netbird is not installed"
exit 1
fi
if [[ $(mdfind "kMDItemContentType == 'com.apple.application-bundle' && kMDItemFSName == '*NetBird UI.app'") ]]; then
echo "Error: NetBird UI is not installed"
exit 1
fi

View File

@@ -1,38 +0,0 @@
name: Test installation Linux
on:
push:
branches:
- main
pull_request:
paths:
- "release_files/install.sh"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
jobs:
install-cli-only:
runs-on: ubuntu-latest
strategy:
matrix:
check_bin_install: [true, false]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Rename apt package
if: ${{ matrix.check_bin_install }}
run: |
sudo mv /usr/bin/apt /usr/bin/apt.bak
sudo mv /usr/bin/apt-get /usr/bin/apt-get.bak
- name: Run install script
run: |
sh ./release_files/install.sh
- name: Run tests
run: |
if ! command -v netbird &> /dev/null; then
echo "Error: netbird is not installed"
exit 1
fi

View File

@@ -4,17 +4,6 @@ on:
push:
tags:
- 'v*'
branches:
- main
pull_request:
env:
SIGN_PIPE_VER: "v0.0.6"
GORELEASER_VER: "v1.14.1"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
jobs:
release:
@@ -29,7 +18,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
go-version: 1.17
-
name: Cache Go modules
uses: actions/cache@v1
@@ -41,9 +30,6 @@ 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
@@ -52,157 +38,28 @@ jobs:
uses: docker/setup-buildx-action@v1
-
name: Login to Docker hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: netbirdio
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Install OS build dependencies
run: sudo apt update && sudo apt install -y -q gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
- name: Install rsrc
run: go install github.com/akavel/rsrc@v0.10.2
- name: Generate windows rsrc amd64
run: rsrc -arch amd64 -ico client/ui/netbird.ico -manifest client/manifest.xml -o client/resources_windows_amd64.syso
- name: Generate windows rsrc arm64
run: rsrc -arch arm64 -ico client/ui/netbird.ico -manifest client/manifest.xml -o client/resources_windows_arm64.syso
- name: Generate windows rsrc arm
run: rsrc -arch arm -ico client/ui/netbird.ico -manifest client/manifest.xml -o client/resources_windows_arm.syso
- name: Generate windows rsrc 386
run: rsrc -arch 386 -ico client/ui/netbird.ico -manifest client/manifest.xml -o client/resources_windows_386.syso
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
version: ${{ env.GORELEASER_VER }}
version: latest
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: upload non tags for debug purposes
uses: actions/upload-artifact@v2
with:
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.19
- 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 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-11
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.19
-
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: Run GoReleaser
id: goreleaser
uses: goreleaser/goreleaser-action@v2
with:
version: ${{ env.GORELEASER_VER }}
args: release --config .goreleaser_ui_darwin.yaml --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
name: upload non tags for debug purposes
uses: actions/upload-artifact@v2
with:
name: release-ui-darwin
path: dist/
retention-days: 3
trigger_windows_signer:
runs-on: ubuntu-latest
needs: [release,release_ui]
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Trigger Windows binaries sign pipeline
name: Trigger Windows binaries sign pipeline
uses: benc-uk/workflow-dispatch@v1
with:
workflow: Sign windows bin and installer
repo: netbirdio/sign-pipelines
ref: ${{ env.SIGN_PIPE_VER }}
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref }}" }'
trigger_darwin_signer:
runs-on: ubuntu-latest
needs: release_ui_darwin
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Trigger Darwin App binaries sign pipeline
uses: benc-uk/workflow-dispatch@v1
with:
workflow: Sign darwin ui app with dispatch
repo: netbirdio/sign-pipelines
ref: ${{ env.SIGN_PIPE_VER }}
repo: wiretrustee/windows-sign-pipeline
ref: v0.0.1
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref }}" }'

View File

@@ -1,99 +0,0 @@
name: Test Docker Compose Linux
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- 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.19.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_DOMAIN: localhost
CI_NETBIRD_AUTH_CLIENT_ID: testing.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_DOMAIN: localhost
CI_NETBIRD_AUTH_CLIENT_ID: testing.client.id
CI_NETBIRD_AUTH_AUDIENCE: testing.ci
CI_NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT: https://example.eu.auth0.com/.well-known/openid-configuration
CI_NETBIRD_USE_AUTH0: true
CI_NETBIRD_AUTH_SUPPORTED_SCOPES: "openid profile email offline_access api email_verified"
CI_NETBIRD_AUTH_AUTHORITY: https://example.eu.auth0.com/
CI_NETBIRD_AUTH_JWT_CERTS: https://example.eu.auth0.com/.well-known/jwks.json
CI_NETBIRD_AUTH_TOKEN_ENDPOINT: https://example.eu.auth0.com/oauth/token
CI_NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT: https://example.eu.auth0.com/oauth/device/code
CI_NETBIRD_AUTH_REDIRECT_URI: "/peers"
CI_NETBIRD_TOKEN_SOURCE: "idToken"
CI_NETBIRD_AUTH_USER_ID_CLAIM: "email"
CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE: "super"
CI_NETBIRD_AUTH_DEVICE_AUTH_SCOPE: "openid email"
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 "$CI_NETBIRD_DOMAIN:33073"
grep AUTH_REDIRECT_URI docker-compose.yml | grep $CI_NETBIRD_AUTH_REDIRECT_URI
grep AUTH_SILENT_REDIRECT_URI docker-compose.yml | egrep 'AUTH_SILENT_REDIRECT_URI=$'
grep LETSENCRYPT_DOMAIN docker-compose.yml | egrep 'LETSENCRYPT_DOMAIN=$'
grep NETBIRD_TOKEN_SOURCE docker-compose.yml | grep $CI_NETBIRD_TOKEN_SOURCE
grep AuthUserIDClaim management.json | grep $CI_NETBIRD_AUTH_USER_ID_CLAIM
grep -A 1 ProviderConfig management.json | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE
grep Scope management.json | grep "$CI_NETBIRD_AUTH_DEVICE_AUTH_SCOPE"
grep UseIDToken management.json | grep false
- name: run docker compose up
working-directory: infrastructure_files
run: |
docker-compose up -d
sleep 5
docker-compose ps
docker-compose logs --tail=20
- 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

8
.gitignore vendored
View File

@@ -1,14 +1,8 @@
.idea
.run
*.iml
dist/
bin/
.env
conf.json
http-cmds.sh
infrastructure_files/management.json
infrastructure_files/docker-compose.yml
*.syso
client/.distfiles/
infrastructure_files/setup.env
.vscode
infrastructure_files/docker-compose.yml

View File

@@ -1,9 +1,10 @@
project_name: netbird
project_name: wiretrustee
builds:
- id: netbird
- id: wiretrustee
dir: client
binary: netbird
binary: wiretrustee
env: [CGO_ENABLED=0]
goos:
- linux
- darwin
@@ -13,7 +14,6 @@ builds:
- amd64
- arm64
- mips
- 386
gomips:
- hardfloat
- softfloat
@@ -22,24 +22,16 @@ builds:
goarch: arm64
- goos: windows
goarch: arm
- goos: windows
goarch: 386
ldflags:
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
mod_timestamp: '{{ .CommitTimestamp }}'
tags:
- load_wgnt_from_rsrc
- load_wintun_from_rsrc
- id: netbird-mgmt
- id: wiretrustee-mgmt
dir: management
env:
- CGO_ENABLED=1
- >-
{{- if eq .Runtime.Goos "linux" }}
{{- if eq .Arch "arm64"}}CC=aarch64-linux-gnu-gcc{{- end }}
{{- if eq .Arch "arm"}}CC=arm-linux-gnueabihf-gcc{{- end }}
{{- end }}
binary: netbird-mgmt
env: [CGO_ENABLED=0]
binary: wiretrustee-mgmt
goos:
- linux
goarch:
@@ -47,13 +39,13 @@ builds:
- arm64
- arm
ldflags:
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
mod_timestamp: '{{ .CommitTimestamp }}'
- id: netbird-signal
- id: wiretrustee-signal
dir: signal
env: [CGO_ENABLED=0]
binary: netbird-signal
binary: wiretrustee-signal
goos:
- linux
goarch:
@@ -61,22 +53,18 @@ builds:
- arm64
- arm
ldflags:
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
mod_timestamp: '{{ .CommitTimestamp }}'
archives:
- builds:
- netbird
- wiretrustee
nfpms:
- maintainer: Netbird <dev@netbird.io>
description: Netbird client.
homepage: https://netbird.io/
id: netbird-deb
bindir: /usr/bin
- maintainer: Wiretrustee <dev@wiretrustee.com>
description: Wiretrustee client.
homepage: https://wiretrustee.com/
id: deb
builds:
- netbird
- wiretrustee
formats:
- deb
@@ -84,13 +72,12 @@ nfpms:
postinstall: "release_files/post_install.sh"
preremove: "release_files/pre_remove.sh"
- maintainer: Netbird <dev@netbird.io>
description: Netbird client.
homepage: https://netbird.io/
id: netbird-rpm
bindir: /usr/bin
- maintainer: Wiretrustee <dev@wiretrustee.com>
description: Wiretrustee client.
homepage: https://wiretrustee.com/
id: rpm
builds:
- netbird
- wiretrustee
formats:
- rpm
@@ -99,9 +86,9 @@ nfpms:
preremove: "release_files/pre_remove.sh"
dockers:
- image_templates:
- netbirdio/netbird:{{ .Version }}-amd64
- wiretrustee/wiretrustee:{{ .Version }}-amd64
ids:
- netbird
- wiretrustee
goarch: amd64
use: buildx
dockerfile: client/Dockerfile
@@ -112,11 +99,11 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- netbirdio/netbird:{{ .Version }}-arm64v8
- wiretrustee/wiretrustee:{{ .Version }}-arm64v8
ids:
- netbird
- wiretrustee
goarch: arm64
use: buildx
dockerfile: client/Dockerfile
@@ -127,11 +114,11 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- netbirdio/netbird:{{ .Version }}-arm
- wiretrustee/wiretrustee:{{ .Version }}-arm
ids:
- netbird
- wiretrustee
goarch: arm
goarm: 6
use: buildx
@@ -143,11 +130,11 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- netbirdio/signal:{{ .Version }}-amd64
- wiretrustee/signal:{{ .Version }}-amd64
ids:
- netbird-signal
- wiretrustee-signal
goarch: amd64
use: buildx
dockerfile: signal/Dockerfile
@@ -158,11 +145,11 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- netbirdio/signal:{{ .Version }}-arm64v8
- wiretrustee/signal:{{ .Version }}-arm64v8
ids:
- netbird-signal
- wiretrustee-signal
goarch: arm64
use: buildx
dockerfile: signal/Dockerfile
@@ -173,11 +160,11 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- netbirdio/signal:{{ .Version }}-arm
- wiretrustee/signal:{{ .Version }}-arm
ids:
- netbird-signal
- wiretrustee-signal
goarch: arm
goarm: 6
use: buildx
@@ -189,11 +176,11 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- netbirdio/management:{{ .Version }}-amd64
- wiretrustee/management:{{ .Version }}-amd64
ids:
- netbird-mgmt
- wiretrustee-mgmt
goarch: amd64
use: buildx
dockerfile: management/Dockerfile
@@ -204,11 +191,11 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- netbirdio/management:{{ .Version }}-arm64v8
- wiretrustee/management:{{ .Version }}-arm64v8
ids:
- netbird-mgmt
- wiretrustee-mgmt
goarch: arm64
use: buildx
dockerfile: management/Dockerfile
@@ -219,11 +206,11 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- netbirdio/management:{{ .Version }}-arm
- wiretrustee/management:{{ .Version }}-arm
ids:
- netbird-mgmt
- wiretrustee-mgmt
goarch: arm
goarm: 6
use: buildx
@@ -235,11 +222,11 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- netbirdio/management:{{ .Version }}-debug-amd64
- wiretrustee/management:{{ .Version }}-debug-amd64
ids:
- netbird-mgmt
- wiretrustee-mgmt
goarch: amd64
use: buildx
dockerfile: management/Dockerfile.debug
@@ -250,11 +237,11 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- netbirdio/management:{{ .Version }}-debug-arm64v8
- wiretrustee/management:{{ .Version }}-debug-arm64v8
ids:
- netbird-mgmt
- wiretrustee-mgmt
goarch: arm64
use: buildx
dockerfile: management/Dockerfile.debug
@@ -265,12 +252,12 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- netbirdio/management:{{ .Version }}-debug-arm
- wiretrustee/management:{{ .Version }}-debug-arm
ids:
- netbird-mgmt
- wiretrustee-mgmt
goarch: arm
goarm: 6
use: buildx
@@ -282,81 +269,78 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- "--label=maintainer=wiretrustee@wiretrustee.com"
docker_manifests:
- name_template: netbirdio/netbird:{{ .Version }}
- name_template: wiretrustee/wiretrustee:{{ .Version }}
image_templates:
- netbirdio/netbird:{{ .Version }}-arm64v8
- netbirdio/netbird:{{ .Version }}-arm
- netbirdio/netbird:{{ .Version }}-amd64
- wiretrustee/wiretrustee:{{ .Version }}-arm64v8
- wiretrustee/wiretrustee:{{ .Version }}-arm
- wiretrustee/wiretrustee:{{ .Version }}-amd64
- name_template: netbirdio/netbird:latest
- name_template: wiretrustee/wiretrustee:latest
image_templates:
- netbirdio/netbird:{{ .Version }}-arm64v8
- netbirdio/netbird:{{ .Version }}-arm
- netbirdio/netbird:{{ .Version }}-amd64
- wiretrustee/wiretrustee:{{ .Version }}-arm64v8
- wiretrustee/wiretrustee:{{ .Version }}-arm
- wiretrustee/wiretrustee:{{ .Version }}-amd64
- name_template: netbirdio/signal:{{ .Version }}
- name_template: wiretrustee/signal:{{ .Version }}
image_templates:
- netbirdio/signal:{{ .Version }}-arm64v8
- netbirdio/signal:{{ .Version }}-arm
- netbirdio/signal:{{ .Version }}-amd64
- wiretrustee/signal:{{ .Version }}-arm64v8
- wiretrustee/signal:{{ .Version }}-arm
- wiretrustee/signal:{{ .Version }}-amd64
- name_template: netbirdio/signal:latest
- name_template: wiretrustee/signal:latest
image_templates:
- netbirdio/signal:{{ .Version }}-arm64v8
- netbirdio/signal:{{ .Version }}-arm
- netbirdio/signal:{{ .Version }}-amd64
- wiretrustee/signal:{{ .Version }}-arm64v8
- wiretrustee/signal:{{ .Version }}-arm
- wiretrustee/signal:{{ .Version }}-amd64
- name_template: netbirdio/management:{{ .Version }}
- name_template: wiretrustee/management:{{ .Version }}
image_templates:
- netbirdio/management:{{ .Version }}-arm64v8
- netbirdio/management:{{ .Version }}-arm
- netbirdio/management:{{ .Version }}-amd64
- wiretrustee/management:{{ .Version }}-arm64v8
- wiretrustee/management:{{ .Version }}-arm
- wiretrustee/management:{{ .Version }}-amd64
- name_template: netbirdio/management:latest
- name_template: wiretrustee/management:latest
image_templates:
- netbirdio/management:{{ .Version }}-arm64v8
- netbirdio/management:{{ .Version }}-arm
- netbirdio/management:{{ .Version }}-amd64
- wiretrustee/management:{{ .Version }}-arm64v8
- wiretrustee/management:{{ .Version }}-arm
- wiretrustee/management:{{ .Version }}-amd64
- name_template: netbirdio/management:debug-latest
- name_template: wiretrustee/management:debug-latest
image_templates:
- netbirdio/management:{{ .Version }}-debug-arm64v8
- netbirdio/management:{{ .Version }}-debug-arm
- netbirdio/management:{{ .Version }}-debug-amd64
- wiretrustee/management:{{ .Version }}-debug-arm64v8
- wiretrustee/management:{{ .Version }}-debug-arm
- wiretrustee/management:{{ .Version }}-debug-amd64
brews:
-
ids:
- default
tap:
owner: netbirdio
name: homebrew-tap
owner: wiretrustee
name: homebrew-client
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
commit_author:
name: Netbird
email: dev@netbird.io
description: Netbird project.
name: Wiretrustee
email: wiretrustee@wiretrustee.com
description: Wiretrustee project.
download_strategy: CurlDownloadStrategy
homepage: https://netbird.io/
homepage: https://wiretrustee.com/
license: "BSD3"
test: |
system "#{bin}/{{ .ProjectName }} version"
system "#{bin}/{{ .ProjectName }} -h"
uploads:
- name: debian
ids:
- netbird-deb
- 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-rpm
- rpm
mode: archive
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
username: dev@wiretrustee.com
method: PUT
method: PUT

View File

@@ -1,98 +0,0 @@
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/version.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/version.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

View File

@@ -1,29 +0,0 @@
project_name: netbird-ui
builds:
- id: netbird-ui-darwin
dir: client/ui
binary: netbird-ui
env: [CGO_ENABLED=1]
goos:
- darwin
goarch:
- amd64
- arm64
gomips:
- hardfloat
- softfloat
ldflags:
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
mod_timestamp: '{{ .CommitTimestamp }}'
tags:
- load_wgnt_from_rsrc
archives:
- builds:
- netbird-ui-darwin
checksum:
name_template: "{{ .ProjectName }}_darwin_checksums.txt"
changelog:
skip: true

View File

@@ -1,3 +1,2 @@
Mikhail Bragin (https://github.com/braginini)
Maycon Santos (https://github.com/mlsmaycon)
Wiretrustee UG (haftungsbeschränkt)

View File

@@ -1,132 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
community@netbird.io.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,218 +0,0 @@
# Contributing to NetBird
Thanks for your interest in contributing to NetBird.
There are many ways that you can contribute:
- Reporting issues
- Updating documentation
- Sharing use cases in slack or Reddit
- Bug fix or feature enhancement
If you haven't already, join our slack workspace [here](https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A), we would love to discuss topics that need community contribution and enhancements to existing features.
## Contents
- [Contributing to NetBird](#contributing-to-netbird)
- [Contents](#contents)
- [Code of conduct](#code-of-conduct)
- [Directory structure](#directory-structure)
- [Development setup](#development-setup)
- [Requirements](#requirements)
- [Local NetBird setup](#local-netbird-setup)
- [Build and start](#build-and-start)
- [Test suite](#test-suite)
- [Checklist before submitting a PR](#checklist-before-submitting-a-pr)
- [Other project repositories](#other-project-repositories)
- [Checklist before submitting a new node](#checklist-before-submitting-a-new-node)
- [Contributor License Agreement](#contributor-license-agreement)
## Code of conduct
This project and everyone participating in it are governed by the Code of
Conduct which can be found in the file [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report
unacceptable behavior to community@netbird.io.
## Directory structure
The NetBird project monorepo is organized to maintain most of its individual dependencies code within their directories, except for a few auxiliary or shared packages.
The most important directories are:
- [/.github](/.github) - Github actions workflow files and issue templates
- [/client](/client) - NetBird agent code
- [/client/cmd](/client/cmd) - NetBird agent cli code
- [/client/internal](/client/internal) - NetBird agent business logic code
- [/client/proto](/client/proto) - NetBird agent daemon GRPC proto files
- [/client/server](/client/server) - NetBird agent daemon code for background execution
- [/client/ui](/client/ui) - NetBird agent UI code
- [/encryption](/encryption) - Contain main encryption code for agent communication
- [/iface](/iface) - Wireguard® interface code
- [/infrastructure_files](/infrastructure_files) - Getting started files containing docker and template scripts
- [/management](/management) - Management service code
- [/management/client](/management/client) - Management service client code which is imported by the agent code
- [/management/proto](/management/proto) - Management service GRPC proto files
- [/management/server](/management/server) - Management service server code
- [/management/server/http](/management/server/http) - Management service REST API code
- [/management/server/idp](/management/server/idp) - Management service IDP management code
- [/release_files](/release_files) - Files that goes into release packages
- [/signal](/signal) - Signal service code
- [/signal/client](/signal/client) - Signal service client code which is imported by the agent code
- [/signal/peer](/signal/peer) - Signal service peer message logic
- [/signal/proto](/signal/proto) - Signal service GRPC proto files
- [/signal/server](/signal/server) - Signal service server code
## Development setup
If you want to contribute to bug fixes or improve existing features, you have to ensure that all needed
dependencies are installed. Here is a short guide on how that can be done.
### Requirements
#### Go 1.19
Follow the installation guide from https://go.dev/
#### UI client - Fyne toolkit
We use the fyne toolkit in our UI client. You can follow its requirement guide to have all its dependencies installed: https://developer.fyne.io/started/#prerequisites
#### gRPC
You can follow the instructions from the quickstarter guide https://grpc.io/docs/languages/go/quickstart/#prerequisites and then run the `generate.sh` files located in each `proto` directory to generate changes.
> **IMPORTANT**: We are very open to contributions that can improve the client daemon protocol. For Signal and Management protocols, please reach out on slack or via github issues with your proposals.
#### Docker
Follow the installation guide from https://docs.docker.com/get-docker/
#### Goreleaser and golangci-lint
We utilize two tools in our Github actions workflows:
- Goreleaser: Used for release packaging. You can follow the installation steps [here](https://goreleaser.com/install/); keep in mind to match the version defined in [release.yml](/.github/workflows/release.yml)
- golangci-lint: Used for linting checks. You can follow the installation steps [here](https://golangci-lint.run/usage/install/); keep in mind to match the version defined in [golangci-lint.yml](/.github/workflows/golangci-lint.yml)
They can be executed from the repository root before every push or PR:
**Goreleaser**
```shell
goreleaser --snapshot --rm-dist
```
**golangci-lint**
```shell
golangci-lint run
```
### Local NetBird setup
> **IMPORTANT**: All the steps below have to get executed at least once to get the development setup up and running!
Now that everything NetBird requires to run is installed, the actual NetBird code can be
checked out and set up:
1. [Fork](https://guides.github.com/activities/forking/#fork) the NetBird repository
2. Clone your forked repository
```
git clone https://github.com/<your_github_username>/netbird.git
```
3. Go into the repository folder
```
cd netbird
```
4. Add the original NetBird repository as `upstream` to your forked repository
```
git remote add upstream https://github.com/netbirdio/netbird.git
```
5. Install all Go dependencies:
```
go mod tidy
```
### Build and start
#### Client
> Windows clients have a Wireguard driver requirement. We provide a bash script that can be executed in WLS 2 with docker support [wireguard_nt.sh](/client/wireguard_nt.sh).
To start NetBird, execute:
```
cd client
# bash wireguard_nt.sh # if windows
go build .
```
To start NetBird the client in the foreground:
```
sudo ./client up --log-level debug --log-file console
```
> On Windows use a powershell with administrator privileges
#### Signal service
To start NetBird's signal, execute:
```
cd signal
go build .
```
To start NetBird the signal service:
```
./signal run --log-level debug --log-file console
```
#### Management service
> You may need to generate a configuration file for management. Follow steps 2 to 5 from our [self-hosting guide](https://netbird.io/docs/getting-started/self-hosting).
To start NetBird's management, execute:
```
cd management
go build .
```
To start NetBird the management service:
```
./management management --log-level debug --log-file console --config ./management.json
```
### Test suite
The tests can be started via:
```
cd netbird
go test -exec sudo ./...
```
> On Windows use a powershell with administrator privileges
## Checklist before submitting a PR
As a critical network service and open-source project, we must enforce a few things before submitting the pull-requests:
- Keep functions as simple as possible, with a single purpose
- Use private functions and constants where possible
- Comment on any new public functions
- Add unit tests for any new public function
> When pushing fixes to the PR comments, please push as separate commits; we will squash the PR before merging, so there is no need to squash it before pushing it, and we are more than okay with 10-100 commits in a single PR. This helps review the fixes to the requested changes.
## Other project repositories
NetBird project is composed of 3 main repositories:
- NetBird: This repository, which contains the code for the agents and control plane services.
- Dashboard: https://github.com/netbirdio/dashboard, contains the Administration UI for the management service
- Documentations: https://github.com/netbirdio/docs, contains the documentation from https://netbird.io/docs
## Contributor License Agreement
That we do not have any potential problems later it is sadly necessary to sign a [Contributor License Agreement](CONTRIBUTOR_LICENSE_AGREEMENT.md). That can be done literally with the push of a button.
A bot will automatically comment on the pull request once it got opened asking for the agreement to be signed. Before it did not get signed it is sadly not possible to merge it in.

View File

@@ -1,148 +0,0 @@
# Contributor License Agreement
We are incredibly thankful for the contributions we receive from the community.
We require our external contributors to sign a Contributor License Agreement ("CLA") in
order to ensure that our projects remain licensed under Free and Open Source licenses such
as BSD-3 while allowing Wiretrustee to build a sustainable business.
Wiretrustee is committed to having a true Open Source Software ("OSS") license for
our software. A CLA enables Wiretrustee to safely commercialize our products
while keeping a standard OSS license with all the rights that license grants to users: the
ability to use the project in their own projects or businesses, to republish modified
source, or to completely fork the project.
This page gives a human-friendly summary of our CLA, details on why we require a CLA, how
contributors can sign our CLA, and more. You may view the full legal CLA document (below).
# Human-friendly summary
This is a human-readable summary of (and not a substitute for) the full agreement (below).
This highlights only some of key terms of the CLA. It has no legal value and you should
carefully review all the terms of the actual CLA before agreeing.
<li>Grant of copyright license. You give Wiretrustee permission to use your copyrighted work
in commercial products.
</li>
<li>Grant of patent license. If your contributed work uses a patent, you give Wiretrustee a
license to use that patent including within commercial products. You also agree that you
have permission to grant this license.
</li>
<li>No Warranty or Support Obligations.
By making a contribution, you are not obligating yourself to provide support for the
contribution, and you are not taking on any warranty obligations or providing any
assurances about how it will perform.
</li>
The CLA does not change the terms of the standard open source license used by our software
such as BSD-3 or MIT.
You are still free to use our projects within your own projects or businesses, republish
modified source, and more.
Please reference the appropriate license for the project you're contributing to to learn
more.
# Why require a CLA?
Agreeing to a CLA explicitly states that you are entitled to provide a contribution, that you cannot withdraw permission
to use your contribution at a later date, and that Wiretrustee has permission to use your contribution in our commercial
products.
This removes any ambiguities or uncertainties caused by not having a CLA and allows users and customers to confidently
adopt our projects. At the same time, the CLA ensures that all contributions to our open source projects are licensed
under the project's respective open source license, such as BSD-3.
Requiring a CLA is a common and well-accepted practice in open source. Major open source projects require CLAs such as
Apache Software Foundation projects, Facebook projects (such as React), Google projects (including Go), Python, Django,
and more. Each of these projects remains licensed under permissive OSS licenses such as MIT, Apache, BSD, and more.
# Signing the CLA
Open a pull request ("PR") to any of our open source projects to sign the CLA. A bot will comment on the PR asking you
to sign the CLA if you haven't already.
Follow the steps given by the bot to sign the CLA. This will require you to log in with GitHub (we only request public
information from your account) and to fill in a few additional details such as your name and email address. We will only
use this information for CLA tracking; none of your submitted information will be used for marketing purposes.
You only have to sign the CLA once. Once you've signed the CLA, future contributions to any Wiretrustee project will not
require you to sign again.
# Legal Terms and Agreement
In order to clarify the intellectual property license granted with Contributions from any person or entity, Wiretrustee
UG (haftungsbeschränkt) ("Wiretrustee") must have a Contributor License Agreement ("CLA") on file that has been signed
by each Contributor, indicating agreement to the license terms below. This license does not change your rights to use
your own Contributions for any other purpose.
You accept and agree to the following terms and conditions for Your present and future Contributions submitted to
Wiretrustee. Except for the license granted herein to Wiretrustee and recipients of software distributed by Wiretrustee,
You reserve all right, title, and interest in and to Your Contributions.
1. Definitions.
```
"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner
that is making this Agreement with Wiretrustee. For legal entities, the entity making a Contribution and all other
entities that control, are controlled by, or are under common control with that entity are considered
to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect,
to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty
percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
```
```
"Contribution" shall mean any original work of authorship, including any modifications or additions to
an existing work, that is or previously has been intentionally submitted by You to Wiretrustee for inclusion in,
or documentation of, any of the products owned or managed by Wiretrustee (the "Work").
For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication
sent to Wiretrustee or its representatives, including but not limited to communication on electronic mailing lists,
source code control systems, and issue tracking systems that are managed by, or on behalf of,
Wiretrustee for the purpose of discussing and improving the Work, but excluding communication that is conspicuously
marked or otherwise designated in writing by You as "Not a Contribution."
```
2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Wiretrustee
and to recipients of software distributed by Wiretrustee a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly
perform, sublicense, and distribute Your Contributions and such derivative works.
3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Wiretrustee and
to recipients of software distributed by Wiretrustee a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import,
and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are
necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which
such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (
including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have
contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity
under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to
intellectual property that you create that includes your Contributions, you represent that you have received
permission to make Contributions on behalf of that employer, that you will have received permission from your current
and future employers for all future Contributions, that your applicable employer has waived such rights for all of
your current and future Contributions to Wiretrustee, or that your employer has executed a separate Corporate CLA
with Wiretrustee.
5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of
others). You represent that Your Contribution submissions include complete details of any third-party license or
other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware
and which are associated with any part of Your Contributions.
6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support.
You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in
writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT,
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
7. Should You wish to submit work that is not Your original creation, You may submit it to Wiretrustee separately from
any Contribution, identifying the complete details of its source and of any license or other restriction (including,
but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and
conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
8. You agree to notify Wiretrustee of any facts or circumstances of which you become aware that would make these
representations inaccurate in any respect.

View File

@@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2022 Wiretrustee UG (haftungsbeschränkt) & AUTHORS
Copyright (c) 2021 Wiretrustee AUTHORS
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

272
README.md
View File

@@ -1,34 +1,23 @@
<p align="center">
<strong>:hatching_chick: New Release! Peer expiration.</strong>
<a href="https://github.com/netbirdio/netbird/releases">
Learn more
</a>
</p>
<br/>
<div align="center">
<p align="center">
<img width="234" src="docs/media/logo-full.png"/>
<img width="250" 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://www.codacy.com/gh/netbirdio/netbird/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=netbirdio/netbird&amp;utm_campaign=Badge_Grade"><img src="https://app.codacy.com/project/badge/Grade/e3013d046aec44cdb7462c8673b00976"/></a>
<br>
<a href="https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">
<img src="https://img.shields.io/badge/slack-@netbird-red.svg?logo=slack"/>
</a>
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
<img src="https://img.shields.io/docker/pulls/wiretrustee/management" />
<img src="https://badgen.net/badge/Open%20Source%3F/Yes%21/blue?icon=github" />
</p>
</div>
<p align="center">
<strong>
Start using NetBird at <a href="https://app.netbird.io/">app.netbird.io</a>
Start using Wiretrustee at <a href="https://app.wiretrustee.com/">app.wiretrustee.com</a>
<br/>
See <a href="https://netbird.io/docs/">Documentation</a>
See <a href="https://docs.wiretrustee.com">Documentation</a>
<br/>
Join our <a href="https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">Slack channel</a>
Join our <a href="https://join.slack.com/t/wiretrustee/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">Slack channel</a>
<br/>
</strong>
@@ -36,79 +25,202 @@
<br>
**NetBird is an open-source VPN management platform built on top of WireGuard® making it easy to create secure private networks for your organization or home.**
**Wiretrustee is an open-source VPN platform built on top of WireGuard® 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.
NetBird uses [NAT traversal techniques](https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment) to automatically create an overlay peer-to-peer network connecting machines regardless of location (home, office, data center, container, cloud, or edge environments), unifying virtual private network management experience.
**Key features:**
- \[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.
- \[x] Network Routes.
- \[x] Private DNS.
- \[x] Network Activity Monitoring.
**Coming soon:**
- \[ ] Mobile clients.
### Secure peer-to-peer VPN with SSO and MFA in minutes
https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
### 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.
### A bit on NetBird internals
- 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.
**Wiretrustee automates Wireguard-based networks, offering a management layer with:**
* Centralized Peer IP management with a UI dashboard.
* Encrypted peer-to-peet connections without a centralized VPN gateway.
* Automatic Peer discovery and configuration.
* UDP hole punching to establish peer-to-peer connections behind NAT, firewall, and without a public static IP.
* Connection relay fallback in case a peer-to-peer connection is not possible.
* Multitenancy (coming soon).
* Client application SSO with MFA (coming soon).
* Access Controls (coming soon).
* Activity Monitoring (coming soon).
* Private DNS (coming baoon)
### Secure peer-to-peer VPN in minutes
<p float="left" align="middle">
<img src="https://netbird.io/docs/img/architecture/high-level-dia.png" width="700"/>
<img src="docs/media/peerA.gif" width="400"/>
<img src="docs/media/peerB.gif" width="400"/>
</p>
See a complete [architecture overview](https://netbird.io/docs/overview/architecture) for details.
**Note**: The `main` branch may be in an *unstable or even broken state* during development. For stable versions, see [releases](https://github.com/wiretrustee/wiretrustee/releases).
### Roadmap
- [Public Roadmap](https://github.com/netbirdio/netbird/projects/2)
Hosted demo version:
[https://app.wiretrustee.com/](https://app.wiretrustee.com/peers).
### Community projects
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird)
- [NetBird installer script](https://github.com/physk/netbird-installer)
[UI Dashboard Repo](https://github.com/wiretrustee/wiretrustee-dashboard)
### Support acknowledgement
In November 2022, NetBird joined the [StartUpSecure program](https://www.forschung-it-sicherheit-kommunikationssysteme.de/foerderung/bekanntmachungen/startup-secure) sponsored by The Federal Ministry of Education and Research of The Federal Republic of Germany. Together with [CISPA Helmholtz Center for Information Security](https://cispa.de/en) NetBird brings the security best practices and simplicity to private networking.
### A bit on Wiretrustee internals
* Wiretrustee features a Management Service that offers peer IP management and network updates distribution (e.g. when a new peer joins the network).
* Wiretrustee 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 devices.
* Peers negotiate connection through [Signal Service](signal/).
* Signal Service uses public Wireguard keys to route messages between peers.
Contents of the messages sent between peers through the signaling server are encrypted with Wireguard keys, making it impossible to inspect them.
* Occasionally, the NAT traversal is unsuccessful due to strict NATs (e.g. mobile carrier-grade NAT). When this occurs the system falls back to the relay server (TURN), 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 Wiretrustee setups.
![CISPA_Logo_BLACK_EN_RZ_RGB (1)](https://user-images.githubusercontent.com/700848/203091324-c6d311a0-22b5-4b05-a288-91cbc6cdcc46.png)
<p float="left" align="middle">
<img src="https://docs.wiretrustee.com/img/architecture/high-level-dia.png" width="700"/>
</p>
### Product Roadmap
- [Public Roadmap](https://github.com/wiretrustee/wiretrustee/projects/2)
- [Public Roadmap Progress Tracking](https://github.com/wiretrustee/wiretrustee/projects/1)
### Client Installation
#### Linux
**APT/Debian**
1. Add the repository:
```shell
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg -y
curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo apt-key add -
echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list
```
2. Install the package
```shell
sudo apt-get update
sudo apt-get install wiretrustee
```
**RPM/Red hat**
1. Add the repository:
```shell
cat <<EOF | sudo tee /etc/yum.repos.d/wiretrustee.repo
[Wiretrustee]
name=Wiretrustee
baseurl=https://pkgs.wiretrustee.com/yum/
enabled=1
gpgcheck=0
gpgkey=https://pkgs.wiretrustee.com/yum/repodata/repomd.xml.key
repo_gpgcheck=1
EOF
```
2. Install the package
```shell
sudo yum install wiretrustee
```
#### MACOS
**Brew install**
1. Download and install Brew at https://brew.sh/
2. Install the client
```shell
brew install wiretrustee/client/wiretrustee
```
**Installation from binary**
1. Checkout Wiretrustee [releases](https://github.com/wiretrustee/wiretrustee/releases/latest)
2. Download the latest release (**Switch VERSION to the latest**):
```shell
curl -o ./wiretrustee_<VERSION>_darwin_amd64.tar.gz https://github.com/wiretrustee/wiretrustee/releases/download/v<VERSION>/wiretrustee_<VERSION>_darwin_amd64.tar.gz
```
3. Decompress
```shell
tar xcf ./wiretrustee_<VERSION>_darwin_amd64.tar.gz
sudo mv wiretrusee /usr/local/bin/wiretrustee
chmod +x /usr/local/bin/wiretrustee
```
After that you may need to add /usr/local/bin in your MAC's PATH environment variable:
````shell
export PATH=$PATH:/usr/local/bin
````
#### Windows
1. Checkout Wiretrustee [releases](https://github.com/wiretrustee/wiretrustee/releases/latest)
2. Download the latest Windows release installer ```wiretrustee_installer_<VERSION>_windows_amd64.exe``` (**Switch VERSION to the latest**):
3. Proceed with installation steps
4. This will install the client in the C:\\Program Files\\Wiretrustee and add the client service
5. After installing, you can follow the [Client Configuration](#Client-Configuration) steps.
> To uninstall the client and service, you can use Add/Remove programs
### Client Configuration
1. Login to the Management Service. You need to have a `setup key` in hand (see ).
For **Unix** systems:
```shell
sudo wiretrustee up --setup-key <SETUP KEY>
```
For **Windows** systems, start powershell as administrator and:
```shell
wiretrustee up --setup-key <SETUP KEY>
```
For **Docker**, you can run with the following command:
```shell
docker run --network host --privileged --rm -d -e WT_SETUP_KEY=<SETUP KEY> -v wiretrustee-client:/etc/wiretrustee wiretrustee/wiretrustee:<TAG>
```
> TAG > 0.3.0 version
Alternatively, if you are hosting your own Management Service provide `--management-url` property pointing to your Management Service:
```shell
sudo wiretrustee up --setup-key <SETUP KEY> --management-url https://localhost:33073
```
> You could also omit `--setup-key` property. In this case the tool will prompt it the key.
2. Check your IP:
For **MACOS** you will just start the service:
````shell
sudo ipconfig getifaddr utun100
````
For **Linux** systems:
```shell
ip addr show wt0
```
For **Windows** systems:
```shell
netsh interface ip show config name="wt0"
```
3. Repeat on other machines.
### Running Dashboard, Management, Signal and Coturn
Wiretrustee uses [Auth0](https://auth0.com) for user authentication and authorization, therefore you will need to create a free account
and configure Auth0 variables in the compose file (dashboard) and in the management config file.
We chose Auth0 to "outsource" the user management part of our platform because we believe that implementing a proper user auth is not a trivial task and requires significant amount of time to make it right. We focused on connectivity instead.
It is worth mentioning that dependency to Auth0 is the only one that cannot be self-hosted.
Configuring Wiretrustee Auth0 integration:
- check [How to run](https://github.com/wiretrustee/wiretrustee-dashboard#how-to-run) to obtain Auth0 environment variables for UI Dashboard
- set these variables in the [environment section of the docker-compose file](https://github.com/wiretrustee/wiretrustee/blob/main/infrastructure_files/docker-compose.yml)
- check [Auth0 Golang API Guide](https://auth0.com/docs/quickstart/backend/golang) to obtain ```AuthIssuer```, ```AuthAudience```, and ```AuthKeysLocation```
- set these properties in the [management config files](https://github.com/wiretrustee/wiretrustee/blob/main/infrastructure_files/management.json#L33)
Under infrastructure_files we have a docker-compose example to run Dashboard, Wiretrustee Management and Signal services, plus an instance of [Coturn](https://github.com/coturn/coturn), it also provides a turnserver.conf file as a simple example of Coturn configuration.
You can edit the turnserver.conf file and change its Realm setting (defaults to wiretrustee.com) to your own domain and user setting (defaults to username1:password1) to **proper credentials**.
The example is set to use the official images from Wiretrustee and Coturn, you can find our documentation to run the signal server in docker in [Running the Signal service](#running-the-signal-service), the management in [Management](./management/README.md), and the Coturn official documentation [here](https://hub.docker.com/r/coturn/coturn).
> Run Coturn at your own risk, we are just providing an example, be sure to follow security best practices and to configure proper credentials as this service can be exploited and you may face large data transfer charges.
Also, if you have an SSL certificate for Coturn, you can modify the docker-compose.yml file to point to its files in your host machine, then switch the domainname to your own SSL domain. If you don't already have an SSL certificate, you can follow [Certbot's](https://certbot.eff.org/docs/intro.html) official documentation
to generate one from [Lets Encrypt](https://letsencrypt.org/), or, we found that the example provided by [BigBlueButton](https://docs.bigbluebutton.org/2.2/setup-turn-server.html#generating-tls-certificates) covers the basics to configure Coturn with Let's Encrypt certs.
> The Wiretrustee Management service can generate and maintain the certificates automatically, all you need to do is run the servicein a host with a public IP, configure a valid DNS record pointing to that IP and uncomment the 443 ports and command lines in the docker-compose.yml file.
Simple docker-composer execution:
````shell
cd infrastructure_files
docker-compose up -d
````
You can check logs by running:
````shell
cd infrastructure_files
docker-compose logs signal
docker-compose logs management
docker-compose logs coturn
````
If you need to stop the services, run the following:
````shell
cd infrastructure_files
docker-compose down
````
### 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).
### Legal
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.
[WireGuard](https://wireguard.com/) is a registered trademark of Jason A. Donenfeld.

View File

@@ -1,12 +0,0 @@
# Security Policy
NetBird's goal is to provide a secure network. If you find a vulnerability or bug, please report it by opening an issue [here](https://github.com/netbirdio/netbird/issues/new?assignees=&labels=&template=bug-issue-report.md&title=) or by contacting us by email.
There has yet to be an official bug bounty program for the NetBird project.
## Supported Versions
- We currently support only the latest version
## Reporting a Vulnerability
Please report security issues to `security@netbird.io`

View File

@@ -1,7 +1,4 @@
FROM gcr.io/distroless/base:debug
ENV NB_FOREGROUND_MODE=true
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
ENV WT_LOG_FILE=console
ENTRYPOINT [ "/go/bin/wiretrustee","up"]
COPY wiretrustee /go/bin/wiretrustee

View File

@@ -1,129 +0,0 @@
package android
import (
"context"
"sync"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/formatter"
"github.com/netbirdio/netbird/iface"
)
// ConnectionListener export internal Listener for mobile
type ConnectionListener interface {
peer.Listener
}
// TunAdapter export internal TunAdapter for mobile
type TunAdapter interface {
iface.TunAdapter
}
// IFaceDiscover export internal IFaceDiscover for mobile
type IFaceDiscover interface {
stdnet.ExternalIFaceDiscover
}
func init() {
formatter.SetLogcatFormatter(log.StandardLogger())
}
// Client struct manage the life circle of background service
type Client struct {
cfgFile string
tunAdapter iface.TunAdapter
iFaceDiscover IFaceDiscover
recorder *peer.Status
ctxCancel context.CancelFunc
ctxCancelLock *sync.Mutex
deviceName string
}
// NewClient instantiate a new Client
func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover) *Client {
lvl, _ := log.ParseLevel("trace")
log.SetLevel(lvl)
return &Client{
cfgFile: cfgFile,
deviceName: deviceName,
tunAdapter: tunAdapter,
iFaceDiscover: iFaceDiscover,
recorder: peer.NewRecorder(""),
ctxCancelLock: &sync.Mutex{},
}
}
// Run start the internal client. It is a blocker function
func (c *Client) Run(urlOpener URLOpener) error {
cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
ConfigPath: c.cfgFile,
})
if err != nil {
return err
}
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
var ctx context.Context
//nolint
ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
c.ctxCancelLock.Lock()
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
defer c.ctxCancel()
c.ctxCancelLock.Unlock()
auth := NewAuthWithConfig(ctx, cfg)
err = auth.login(urlOpener)
if err != nil {
return err
}
// todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx)
return internal.RunClient(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover)
}
// Stop the internal client and free the resources
func (c *Client) Stop() {
c.ctxCancelLock.Lock()
defer c.ctxCancelLock.Unlock()
if c.ctxCancel == nil {
return
}
c.ctxCancel()
}
// PeersList return with the list of the PeerInfos
func (c *Client) PeersList() *PeerInfoArray {
fullStatus := c.recorder.GetFullStatus()
peerInfos := make([]PeerInfo, len(fullStatus.Peers))
for n, p := range fullStatus.Peers {
pi := PeerInfo{
p.IP,
p.FQDN,
p.ConnStatus.String(),
p.Direct,
}
peerInfos[n] = pi
}
return &PeerInfoArray{items: peerInfos}
}
// SetConnectionListener set the network connection listener
func (c *Client) SetConnectionListener(listener ConnectionListener) {
c.recorder.SetConnectionListener(listener)
}
// RemoveConnectionListener remove connection listener
func (c *Client) RemoveConnectionListener() {
c.recorder.RemoveConnectionListener()
}

View File

@@ -1,229 +0,0 @@
package android
import (
"context"
"fmt"
"time"
"github.com/cenkalti/backoff/v4"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/cmd"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/client/internal"
)
// SSOListener is async listener for mobile framework
type SSOListener interface {
OnSuccess(bool)
OnError(error)
}
// ErrListener is async listener for mobile framework
type ErrListener interface {
OnSuccess()
OnError(error)
}
// URLOpener it is a callback interface. The Open function will be triggered if
// the backend want to show an url for the user
type URLOpener interface {
Open(string)
}
// Auth can register or login new client
type Auth struct {
ctx context.Context
config *internal.Config
cfgPath string
}
// NewAuth instantiate Auth struct and validate the management URL
func NewAuth(cfgPath string, mgmURL string) (*Auth, error) {
inputCfg := internal.ConfigInput{
ManagementURL: mgmURL,
}
cfg, err := internal.CreateInMemoryConfig(inputCfg)
if err != nil {
return nil, err
}
return &Auth{
ctx: context.Background(),
config: cfg,
cfgPath: cfgPath,
}, nil
}
// NewAuthWithConfig instantiate Auth based on existing config
func NewAuthWithConfig(ctx context.Context, config *internal.Config) *Auth {
return &Auth{
ctx: ctx,
config: config,
}
}
// SaveConfigIfSSOSupported test the connectivity with the management server by retrieving the server device flow info.
// If it returns a flow info than save the configuration and return true. If it gets a codes.NotFound, it means that SSO
// is not supported and returns false without saving the configuration. For other errors return false.
func (a *Auth) SaveConfigIfSSOSupported(listener SSOListener) {
go func() {
sso, err := a.saveConfigIfSSOSupported()
if err != nil {
listener.OnError(err)
} else {
listener.OnSuccess(sso)
}
}()
}
func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
supportsSSO := true
err := a.withBackOff(a.ctx, func() (err error) {
_, err = internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.NotFound {
supportsSSO = false
err = nil
}
return err
})
if !supportsSSO {
return false, nil
}
if err != nil {
return false, fmt.Errorf("backoff cycle failed: %v", err)
}
err = internal.WriteOutConfig(a.cfgPath, a.config)
return true, err
}
// LoginWithSetupKeyAndSaveConfig test the connectivity with the management server with the setup key.
func (a *Auth) LoginWithSetupKeyAndSaveConfig(resultListener ErrListener, setupKey string, deviceName string) {
go func() {
err := a.loginWithSetupKeyAndSaveConfig(setupKey, deviceName)
if err != nil {
resultListener.OnError(err)
} else {
resultListener.OnSuccess()
}
}()
}
func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string) error {
//nolint
ctxWithValues := context.WithValue(a.ctx, system.DeviceNameCtxKey, deviceName)
err := a.withBackOff(a.ctx, func() error {
backoffErr := internal.Login(ctxWithValues, a.config, setupKey, "")
if s, ok := gstatus.FromError(backoffErr); ok && (s.Code() == codes.PermissionDenied) {
// we got an answer from management, exit backoff earlier
return backoff.Permanent(backoffErr)
}
return backoffErr
})
if err != nil {
return fmt.Errorf("backoff cycle failed: %v", err)
}
return internal.WriteOutConfig(a.cfgPath, a.config)
}
// Login try register the client on the server
func (a *Auth) Login(resultListener ErrListener, urlOpener URLOpener) {
go func() {
err := a.login(urlOpener)
if err != nil {
resultListener.OnError(err)
} else {
resultListener.OnSuccess()
}
}()
}
func (a *Auth) login(urlOpener URLOpener) error {
var needsLogin bool
// check if we need to generate JWT token
err := a.withBackOff(a.ctx, func() (err error) {
needsLogin, err = internal.IsLoginRequired(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config.SSHKey)
return
})
if err != nil {
return fmt.Errorf("backoff cycle failed: %v", err)
}
jwtToken := ""
if needsLogin {
tokenInfo, err := a.foregroundGetTokenInfo(urlOpener)
if err != nil {
return fmt.Errorf("interactive sso login failed: %v", err)
}
jwtToken = tokenInfo.GetTokenToUse()
}
err = a.withBackOff(a.ctx, func() error {
err := internal.Login(a.ctx, a.config, "", jwtToken)
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
return nil
}
return err
})
if err != nil {
return fmt.Errorf("backoff cycle failed: %v", err)
}
return nil
}
func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*internal.TokenInfo, error) {
providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
if err != nil {
s, ok := gstatus.FromError(err)
if ok && s.Code() == codes.NotFound {
return nil, fmt.Errorf("no SSO provider returned from management. " +
"If you are using hosting Netbird see documentation at " +
"https://github.com/netbirdio/netbird/tree/main/management for details")
} else if ok && s.Code() == codes.Unimplemented {
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
"please update your servver or use Setup Keys to login", a.config.ManagementURL)
} else {
return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err)
}
}
hostedClient := internal.NewHostedDeviceFlow(providerConfig.ProviderConfig)
flowInfo, err := hostedClient.RequestDeviceCode(context.TODO())
if err != nil {
return nil, fmt.Errorf("getting a request device code failed: %v", err)
}
go urlOpener.Open(flowInfo.VerificationURIComplete)
waitTimeout := time.Duration(flowInfo.ExpiresIn)
waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout*time.Second)
defer cancel()
tokenInfo, err := hostedClient.WaitToken(waitCTX, flowInfo)
if err != nil {
return nil, fmt.Errorf("waiting for browser login failed: %v", err)
}
return &tokenInfo, nil
}
func (a *Auth) withBackOff(ctx context.Context, bf func() error) error {
return backoff.RetryNotify(
bf,
backoff.WithContext(cmd.CLIBackOffSettings, ctx),
func(err error, duration time.Duration) {
log.Warnf("retrying Login to the Management service in %v due to error %v", duration, err)
})
}

View File

@@ -1,37 +0,0 @@
package android
// PeerInfo describe information about the peers. It designed for the UI usage
type PeerInfo struct {
IP string
FQDN string
ConnStatus string // Todo replace to enum
Direct bool
}
// PeerInfoCollection made for Java layer to get non default types as collection
type PeerInfoCollection interface {
Add(s string) PeerInfoCollection
Get(i int) string
Size() int
}
// PeerInfoArray is the implementation of the PeerInfoCollection
type PeerInfoArray struct {
items []PeerInfo
}
// Add new PeerInfo to the collection
func (array PeerInfoArray) Add(s PeerInfo) PeerInfoArray {
array.items = append(array.items, s)
return array
}
// Get return an element of the collection
func (array PeerInfoArray) Get(i int) *PeerInfo {
return &array.items[i]
}
// Size return with the size of the collection
func (array PeerInfoArray) Size() int {
return len(array.items)
}

View File

@@ -1,78 +0,0 @@
package android
import (
"github.com/netbirdio/netbird/client/internal"
)
// Preferences export a subset of the internal config for gomobile
type Preferences struct {
configInput internal.ConfigInput
}
// NewPreferences create new Preferences instance
func NewPreferences(configPath string) *Preferences {
ci := internal.ConfigInput{
ConfigPath: configPath,
}
return &Preferences{ci}
}
// GetManagementURL read url from config file
func (p *Preferences) GetManagementURL() (string, error) {
if p.configInput.ManagementURL != "" {
return p.configInput.ManagementURL, nil
}
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
return cfg.ManagementURL.String(), err
}
// SetManagementURL store the given url and wait for commit
func (p *Preferences) SetManagementURL(url string) {
p.configInput.ManagementURL = url
}
// GetAdminURL read url from config file
func (p *Preferences) GetAdminURL() (string, error) {
if p.configInput.AdminURL != "" {
return p.configInput.AdminURL, nil
}
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
return cfg.AdminURL.String(), err
}
// SetAdminURL store the given url and wait for commit
func (p *Preferences) SetAdminURL(url string) {
p.configInput.AdminURL = url
}
// GetPreSharedKey read preshared key from config file
func (p *Preferences) GetPreSharedKey() (string, error) {
if p.configInput.PreSharedKey != nil {
return *p.configInput.PreSharedKey, nil
}
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
return cfg.PreSharedKey, err
}
// SetPreSharedKey store the given key and wait for commit
func (p *Preferences) SetPreSharedKey(key string) {
p.configInput.PreSharedKey = &key
}
// Commit write out the changes into config file
func (p *Preferences) Commit() error {
_, err := internal.UpdateOrCreateConfig(p.configInput)
return err
}

View File

@@ -1,120 +0,0 @@
package android
import (
"path/filepath"
"testing"
"github.com/netbirdio/netbird/client/internal"
)
func TestPreferences_DefaultValues(t *testing.T) {
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
p := NewPreferences(cfgFile)
defaultVar, err := p.GetAdminURL()
if err != nil {
t.Fatalf("failed to read default value: %s", err)
}
if defaultVar != internal.DefaultAdminURL {
t.Errorf("invalid default admin url: %s", defaultVar)
}
defaultVar, err = p.GetManagementURL()
if err != nil {
t.Fatalf("failed to read default management URL: %s", err)
}
if defaultVar != internal.DefaultManagementURL {
t.Errorf("invalid default management url: %s", defaultVar)
}
var preSharedKey string
preSharedKey, err = p.GetPreSharedKey()
if err != nil {
t.Fatalf("failed to read default preshared key: %s", err)
}
if preSharedKey != "" {
t.Errorf("invalid preshared key: %s", preSharedKey)
}
}
func TestPreferences_ReadUncommitedValues(t *testing.T) {
exampleString := "exampleString"
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
p := NewPreferences(cfgFile)
p.SetAdminURL(exampleString)
resp, err := p.GetAdminURL()
if err != nil {
t.Fatalf("failed to read admin url: %s", err)
}
if resp != exampleString {
t.Errorf("unexpected admin url: %s", resp)
}
p.SetManagementURL(exampleString)
resp, err = p.GetManagementURL()
if err != nil {
t.Fatalf("failed to read managmenet url: %s", err)
}
if resp != exampleString {
t.Errorf("unexpected managemenet url: %s", resp)
}
p.SetPreSharedKey(exampleString)
resp, err = p.GetPreSharedKey()
if err != nil {
t.Fatalf("failed to read preshared key: %s", err)
}
if resp != exampleString {
t.Errorf("unexpected preshared key: %s", resp)
}
}
func TestPreferences_Commit(t *testing.T) {
exampleURL := "https://myurl.com:443"
examplePresharedKey := "topsecret"
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
p := NewPreferences(cfgFile)
p.SetAdminURL(exampleURL)
p.SetManagementURL(exampleURL)
p.SetPreSharedKey(examplePresharedKey)
err := p.Commit()
if err != nil {
t.Fatalf("failed to save changes: %s", err)
}
p = NewPreferences(cfgFile)
resp, err := p.GetAdminURL()
if err != nil {
t.Fatalf("failed to read admin url: %s", err)
}
if resp != exampleURL {
t.Errorf("unexpected admin url: %s", resp)
}
resp, err = p.GetManagementURL()
if err != nil {
t.Fatalf("failed to read managmenet url: %s", err)
}
if resp != exampleURL {
t.Errorf("unexpected managemenet url: %s", resp)
}
resp, err = p.GetPreSharedKey()
if err != nil {
t.Fatalf("failed to read preshared key: %s", err)
}
if resp != examplePresharedKey {
t.Errorf("unexpected preshared key: %s", resp)
}
}

View File

@@ -1,46 +0,0 @@
package cmd
import (
"context"
"github.com/netbirdio/netbird/util"
"time"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/proto"
)
var downCmd = &cobra.Command{
Use: "down",
Short: "down netbird connections",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
log.Errorf("failed initializing log %v", err)
return err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
log.Errorf("failed to connect to service CLI interface %v", err)
return err
}
defer conn.Close()
daemonClient := proto.NewDaemonServiceClient(conn)
if _, err := daemonClient.Down(ctx, &proto.DownRequest{}); err != nil {
log.Errorf("call service down method: %v", err)
return err
}
return nil
},
}

View File

@@ -1,204 +1,147 @@
package cmd
import (
"bufio"
"context"
"fmt"
"time"
"github.com/skratchdot/open-golang/open"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/util"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/proto"
"github.com/wiretrustee/wiretrustee/client/internal"
mgm "github.com/wiretrustee/wiretrustee/management/client"
mgmProto "github.com/wiretrustee/wiretrustee/management/proto"
"github.com/wiretrustee/wiretrustee/util"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"os"
)
var loginCmd = &cobra.Command{
Use: "login",
Short: "login to the Netbird Management Service (first run)",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
var (
loginCmd = &cobra.Command{
Use: "login",
Short: "login to the Wiretrustee Management Service (first run)",
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)
}
ctx := internal.CtxInitState(context.Background())
// workaround to run without service
if logFile == "console" {
err = handleRebrand(cmd)
err := util.InitLog(logLevel, logFile)
if err != nil {
log.Errorf("failed initializing log %v", err)
return err
}
config, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: configPath,
PreSharedKey: &preSharedKey,
})
config, err := internal.GetConfig(managementURL, configPath, preSharedKey)
if err != nil {
return fmt.Errorf("get config file: %v", err)
log.Errorf("failed getting config %s %v", configPath, err)
return err
}
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
err = foregroundLogin(ctx, cmd, config, setupKey)
//validate our peer's Wireguard PRIVATE key
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
if err != nil {
return fmt.Errorf("foreground login failed: %v", err)
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
return err
}
cmd.Println("Logging successfully")
return nil
}
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
return fmt.Errorf("failed to connect to daemon error: %v\n"+
"If the daemon is not running please run: "+
"\nnetbird service install \nnetbird service start\n", err)
}
defer conn.Close()
ctx := context.Background()
client := proto.NewDaemonServiceClient(conn)
loginRequest := proto.LoginRequest{
SetupKey: setupKey,
PreSharedKey: preSharedKey,
ManagementUrl: managementURL,
}
var loginErr error
var loginResp *proto.LoginResponse
err = WithBackOff(func() error {
var backOffErr error
loginResp, backOffErr = client.Login(ctx, &loginRequest)
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
s.Code() == codes.PermissionDenied ||
s.Code() == codes.NotFound ||
s.Code() == codes.Unimplemented) {
loginErr = backOffErr
return nil
mgmTlsEnabled := false
if config.ManagementURL.Scheme == "https" {
mgmTlsEnabled = true
}
return backOffErr
})
if err != nil {
return fmt.Errorf("login backoff cycle failed: %v", err)
}
if loginErr != nil {
return fmt.Errorf("login failed: %v", loginErr)
}
if loginResp.NeedsSSOLogin {
openURL(cmd, loginResp.VerificationURIComplete)
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
log.Debugf("connecting to Management Service %s", config.ManagementURL.String())
mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
if err != nil {
return fmt.Errorf("waiting sso login failed with: %v", err)
log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), err)
return err
}
}
log.Debugf("connected to anagement Service %s", config.ManagementURL.String())
cmd.Println("Logging successfully")
return nil
},
}
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.Config, setupKey string) error {
needsLogin := false
err := WithBackOff(func() error {
err := internal.Login(ctx, config, "", "")
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
needsLogin = true
return nil
}
return err
})
if err != nil {
return fmt.Errorf("backoff cycle failed: %v", err)
}
jwtToken := ""
if setupKey == "" && needsLogin {
tokenInfo, err := foregroundGetTokenInfo(ctx, cmd, config)
if err != nil {
return fmt.Errorf("interactive sso login failed: %v", err)
}
jwtToken = tokenInfo.GetTokenToUse()
}
err = WithBackOff(func() error {
err := internal.Login(ctx, config, setupKey, jwtToken)
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
return nil
}
return err
})
if err != nil {
return fmt.Errorf("backoff cycle failed: %v", err)
}
return nil
}
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*internal.TokenInfo, error) {
providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
if err != nil {
s, ok := gstatus.FromError(err)
if ok && s.Code() == codes.NotFound {
return nil, fmt.Errorf("no SSO provider returned from management. " +
"If you are using hosting Netbird see documentation at " +
"https://github.com/netbirdio/netbird/tree/main/management for details")
} else if ok && s.Code() == codes.Unimplemented {
mgmtURL := managementURL
if mgmtURL == "" {
mgmtURL = internal.DefaultManagementURL
serverKey, err := mgmClient.GetServerPublicKey()
if err != nil {
log.Errorf("failed while getting Management Service public key: %v", err)
return err
}
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
"please update your servver or use Setup Keys to login", mgmtURL)
_, err = loginPeer(*serverKey, mgmClient, setupKey)
if err != nil {
log.Errorf("failed logging-in peer on Management Service : %v", err)
return err
}
err = mgmClient.Close()
if err != nil {
log.Errorf("failed closing Management Service client: %v", err)
return err
}
return nil
},
}
)
// loginPeer attempts to login to Management Service. If peer wasn't registered, tries the registration flow.
func loginPeer(serverPublicKey wgtypes.Key, client *mgm.Client, setupKey string) (*mgmProto.LoginResponse, error) {
loginResp, err := client.Login(serverPublicKey)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
log.Debugf("peer registration required")
return registerPeer(serverPublicKey, client, setupKey)
} else {
return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err)
return nil, err
}
}
hostedClient := internal.NewHostedDeviceFlow(providerConfig.ProviderConfig)
log.Info("peer has successfully logged-in to Management Service")
flowInfo, err := hostedClient.RequestDeviceCode(context.TODO())
if err != nil {
return nil, fmt.Errorf("getting a request device code failed: %v", err)
}
openURL(cmd, flowInfo.VerificationURIComplete)
waitTimeout := time.Duration(flowInfo.ExpiresIn)
waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout*time.Second)
defer c()
tokenInfo, err := hostedClient.WaitToken(waitCTX, flowInfo)
if err != nil {
return nil, fmt.Errorf("waiting for browser login failed: %v", err)
}
return &tokenInfo, nil
return loginResp, nil
}
func openURL(cmd *cobra.Command, verificationURIComplete string) {
err := open.Run(verificationURIComplete)
cmd.Printf("Please do the SSO login in your browser. \n" +
"If your browser didn't open automatically, use this URL to log in:\n\n" +
" " + verificationURIComplete + " \n\n")
if err != nil {
cmd.Printf("Alternatively, you may want to use a setup key, see:\n\n https://www.netbird.io/docs/overview/setup-keys\n")
// 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(serverPublicKey wgtypes.Key, client *mgm.Client, setupKey string) (*mgmProto.LoginResponse, error) {
var err error
if setupKey == "" {
setupKey, err = promptPeerSetupKey()
if err != nil {
log.Errorf("failed getting setup key from user: %s", err)
return nil, err
}
}
validSetupKey, err := uuid.Parse(setupKey)
if err != nil {
return nil, err
}
log.Debugf("sending peer registration request to Management Service")
loginResp, err := client.Register(serverPublicKey, validSetupKey.String())
if err != nil {
log.Errorf("failed registering peer %v", err)
return nil, err
}
log.Infof("peer has been successfully registered on Management Service")
return loginResp, nil
}
// promptPeerSetupKey prompts user to enter Setup Key
func promptPeerSetupKey() (string, error) {
fmt.Print("Enter setup key: ")
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
input := s.Text()
if input != "" {
return input, nil
}
fmt.Println("Specified key is empty, try again:")
}
return "", s.Err()
}

View File

@@ -2,16 +2,34 @@ package cmd
import (
"fmt"
"github.com/wiretrustee/wiretrustee/client/internal"
"github.com/wiretrustee/wiretrustee/iface"
mgmt "github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/util"
"path/filepath"
"strings"
"testing"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/util"
)
var mgmAddr string
func TestLogin_Start(t *testing.T) {
config := &mgmt.Config{}
_, err := util.ReadJson("../testdata/management.json", config)
if err != nil {
t.Fatal(err)
}
testDir := t.TempDir()
config.Datadir = testDir
err = util.CopyFileContents("../testdata/store.json", filepath.Join(testDir, "store.json"))
if err != nil {
t.Fatal(err)
}
_, listener := startManagement(config, t)
mgmAddr = listener.Addr().String()
}
func TestLogin(t *testing.T) {
mgmAddr := startTestingServices(t)
tempDir := t.TempDir()
confPath := tempDir + "/config.json"
@@ -20,8 +38,6 @@ func TestLogin(t *testing.T) {
"login",
"--config",
confPath,
"--log-file",
"console",
"--setup-key",
strings.ToUpper("a2c8e62b-38f5-4553-b31e-dd66c696cebb"),
"--management-url",
@@ -44,7 +60,7 @@ func TestLogin(t *testing.T) {
}
if actualConf.WgIface != iface.WgInterfaceDefault {
t.Errorf("expected WgIfaceName %s got %s", iface.WgInterfaceDefault, actualConf.WgIface)
t.Errorf("expected WgIface %s got %s", iface.WgInterfaceDefault, actualConf.WgIface)
}
if len(actualConf.PrivateKey) == 0 {

View File

@@ -1,308 +1,101 @@
package cmd
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/signal"
"path"
"runtime"
"strings"
"syscall"
"time"
"github.com/cenkalti/backoff/v4"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/netbirdio/netbird/client/internal"
)
const (
externalIPMapFlag = "external-ip-map"
dnsResolverAddress = "dns-resolver-address"
"github.com/wiretrustee/wiretrustee/client/internal"
"os"
"os/signal"
"runtime"
"strings"
"syscall"
)
var (
configPath string
defaultConfigPathDir string
defaultConfigPath string
oldDefaultConfigPathDir string
oldDefaultConfigPath string
logLevel string
defaultLogFileDir string
defaultLogFile string
oldDefaultLogFileDir string
oldDefaultLogFile string
logFile string
daemonAddr string
managementURL string
adminURL string
setupKey string
preSharedKey string
natExternalIPs []string
customDNSAddress string
rootCmd = &cobra.Command{
Use: "netbird",
Short: "",
Long: "",
SilenceUsage: true,
configPath string
defaultConfigPath string
logLevel string
defaultLogFile string
logFile string
managementURL string
setupKey string
preSharedKey string
rootCmd = &cobra.Command{
Use: "wiretrustee",
Short: "",
Long: "",
}
// Execution control channel for stopCh signal
stopCh chan int
cleanupCh chan struct{}
)
// Execute executes the root command.
func Execute() error {
return rootCmd.Execute()
}
func init() {
defaultConfigPathDir = "/etc/netbird/"
defaultLogFileDir = "/var/log/netbird/"
oldDefaultConfigPathDir = "/etc/wiretrustee/"
oldDefaultLogFileDir = "/var/log/wiretrustee/"
stopCh = make(chan int)
cleanupCh = make(chan struct{})
defaultConfigPath = "/etc/wiretrustee/config.json"
defaultLogFile = "/var/log/wiretrustee/client.log"
if runtime.GOOS == "windows" {
defaultConfigPathDir = os.Getenv("PROGRAMDATA") + "\\Netbird\\"
defaultLogFileDir = os.Getenv("PROGRAMDATA") + "\\Netbird\\"
oldDefaultConfigPathDir = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\"
oldDefaultLogFileDir = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\"
defaultConfigPath = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "config.json"
defaultLogFile = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "client.log"
}
defaultConfigPath = defaultConfigPathDir + "config.json"
defaultLogFile = defaultLogFileDir + "client.log"
oldDefaultConfigPath = oldDefaultConfigPathDir + "config.json"
oldDefaultLogFile = oldDefaultLogFileDir + "client.log"
defaultDaemonAddr := "unix:///var/run/netbird.sock"
if runtime.GOOS == "windows" {
defaultDaemonAddr = "tcp://127.0.0.1:41731"
}
rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL))
rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL))
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location")
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level")
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the the log will be output to stdout")
rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)")
rootCmd.PersistentFlags().StringVar(&managementURL, "management-url", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.ManagementURLDefault().String()))
rootCmd.PersistentFlags().StringVar(&configPath, "config", defaultConfigPath, "Wiretrustee config file location")
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "sets Wiretrustee log level")
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Wiretrustee log path. If console is specified the the log will be output to stdout")
rootCmd.PersistentFlags().StringVar(&setupKey, "setup-key", "", "Setup key obtained from the Management Service Dashboard (used to register peer)")
rootCmd.PersistentFlags().StringVar(&preSharedKey, "preshared-key", "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.")
rootCmd.AddCommand(serviceCmd)
rootCmd.AddCommand(upCmd)
rootCmd.AddCommand(downCmd)
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
upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil,
`Sets external IPs maps between local addresses and interfaces.`+
`You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+
`An empty string "" clears the previous configuration. `+
`E.g. --external-ip-map 12.34.56.78/10.0.0.1 or --external-ip-map 12.34.56.200,12.34.56.78/10.0.0.1,12.34.56.80/eth1 `+
`or --external-ip-map ""`,
)
upCmd.PersistentFlags().StringVar(&customDNSAddress, dnsResolverAddress, "",
`Sets a custom address for NetBird's local DNS resolver. `+
`If set, the agent won't attempt to discover the best ip and port to listen on. `+
`An empty string "" clears the previous configuration. `+
`E.g. --dns-resolver-address 127.0.0.1:5053 or --dns-resolver-address ""`,
)
}
// SetupCloseHandler handles SIGTERM signal and exits with success
func SetupCloseHandler(ctx context.Context, cancel context.CancelFunc) {
termCh := make(chan os.Signal, 1)
signal.Notify(termCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
func SetupCloseHandler() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
done := ctx.Done()
select {
case <-done:
case <-termCh:
for range c {
log.Info("shutdown signal received")
stopCh <- 0
}
log.Info("shutdown signal received")
cancel()
}()
}
// SetFlagsFromEnvVars reads and updates flag values from environment variables with prefix WT_
func SetFlagsFromEnvVars(cmd *cobra.Command) {
flags := cmd.PersistentFlags()
func SetFlagsFromEnvVars() {
flags := rootCmd.PersistentFlags()
flags.VisitAll(func(f *pflag.Flag) {
oldEnvVar := FlagNameToEnvVar(f.Name, "WT_")
if value, present := os.LookupEnv(oldEnvVar); present {
envVar := FlagNameToEnvVar(f.Name)
if value, present := os.LookupEnv(envVar); present {
err := flags.Set(f.Name, value)
if err != nil {
log.Infof("unable to configure flag %s using variable %s, err: %v", f.Name, oldEnvVar, err)
}
}
newEnvVar := FlagNameToEnvVar(f.Name, "NB_")
if value, present := os.LookupEnv(newEnvVar); present {
err := flags.Set(f.Name, value)
if err != nil {
log.Infof("unable to configure flag %s using variable %s, err: %v", f.Name, newEnvVar, err)
log.Infof("unable to configure flag %s using variable %s, err: %v", f.Name, envVar, err)
}
}
})
}
// FlagNameToEnvVar converts flag name to environment var name adding a prefix,
// replacing dashes and making all uppercase (e.g. setup-keys is converted to NB_SETUP_KEYS according to the input prefix)
func FlagNameToEnvVar(cmdFlag string, prefix string) string {
parsed := strings.ReplaceAll(cmdFlag, "-", "_")
// replacing dashes and making all uppercase (e.g. setup-keys is converted to WT_SETUP_KEYS)
func FlagNameToEnvVar(f string) string {
prefix := "WT_"
parsed := strings.ReplaceAll(f, "-", "_")
upper := strings.ToUpper(parsed)
return prefix + upper
}
// DialClientGRPCServer returns client connection to the dameno server.
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
return grpc.DialContext(
ctx,
strings.TrimPrefix(addr, "tcp://"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
}
// WithBackOff execute function in backoff cycle.
func WithBackOff(bf func() error) error {
return backoff.RetryNotify(bf, CLIBackOffSettings, func(err error, duration time.Duration) {
log.Warnf("retrying Login to the Management service in %v due to error %v", duration, err)
})
}
// CLIBackOffSettings is default backoff settings for CLI commands.
var CLIBackOffSettings = &backoff.ExponentialBackOff{
InitialInterval: time.Second,
RandomizationFactor: backoff.DefaultRandomizationFactor,
Multiplier: backoff.DefaultMultiplier,
MaxInterval: 10 * time.Second,
MaxElapsedTime: 30 * time.Second,
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}
func handleRebrand(cmd *cobra.Command) error {
var err error
if logFile == defaultLogFile {
if migrateToNetbird(oldDefaultLogFile, defaultLogFile) {
cmd.Printf("will copy Log dir %s and its content to %s\n", oldDefaultLogFileDir, defaultLogFileDir)
err = cpDir(oldDefaultLogFileDir, defaultLogFileDir)
if err != nil {
return err
}
}
}
if configPath == defaultConfigPath {
if migrateToNetbird(oldDefaultConfigPath, defaultConfigPath) {
cmd.Printf("will copy Config dir %s and its content to %s\n", oldDefaultConfigPathDir, defaultConfigPathDir)
err = cpDir(oldDefaultConfigPathDir, defaultConfigPathDir)
if err != nil {
return err
}
}
}
return nil
}
func cpFile(src, dst string) error {
var err error
var srcfd *os.File
var dstfd *os.File
var srcinfo os.FileInfo
if srcfd, err = os.Open(src); err != nil {
return err
}
defer srcfd.Close()
if dstfd, err = os.Create(dst); err != nil {
return err
}
defer dstfd.Close()
if _, err = io.Copy(dstfd, srcfd); err != nil {
return err
}
if srcinfo, err = os.Stat(src); err != nil {
return err
}
return os.Chmod(dst, srcinfo.Mode())
}
func copySymLink(source, dest string) error {
link, err := os.Readlink(source)
if err != nil {
return err
}
return os.Symlink(link, dest)
}
func cpDir(src string, dst string) error {
var err error
var fds []os.DirEntry
var srcinfo os.FileInfo
if srcinfo, err = os.Stat(src); err != nil {
return err
}
if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
return err
}
if fds, err = os.ReadDir(src); err != nil {
return err
}
for _, fd := range fds {
srcfp := path.Join(src, fd.Name())
dstfp := path.Join(dst, fd.Name())
fileInfo, err := os.Stat(srcfp)
if err != nil {
return fmt.Errorf("fouldn't get fileInfo; %v", err)
}
switch fileInfo.Mode() & os.ModeType {
case os.ModeSymlink:
if err = copySymLink(srcfp, dstfp); err != nil {
return fmt.Errorf("failed to copy from %s to %s; %v", srcfp, dstfp, err)
}
case os.ModeDir:
if err = cpDir(srcfp, dstfp); err != nil {
return fmt.Errorf("failed to copy from %s to %s; %v", srcfp, dstfp, err)
}
default:
if err = cpFile(srcfp, dstfp); err != nil {
return fmt.Errorf("failed to copy from %s to %s; %v", srcfp, dstfp, err)
}
}
}
return nil
}
func migrateToNetbird(oldPath, newPath string) bool {
_, errOld := os.Stat(oldPath)
_, errNew := os.Stat(newPath)
if errors.Is(errOld, fs.ErrNotExist) || errNew == nil {
return false
}
return true
}

View File

@@ -1,36 +0,0 @@
package cmd
import (
"fmt"
"io"
"testing"
)
func TestInitCommands(t *testing.T) {
helpFlag := "-h"
commandArgs := [][]string{{"root", helpFlag}}
for _, command := range rootCmd.Commands() {
commandArgs = append(commandArgs, []string{command.Name(), command.Name(), helpFlag})
for _, subcommand := range command.Commands() {
commandArgs = append(commandArgs, []string{command.Name() + " " + subcommand.Name(), command.Name(), subcommand.Name(), helpFlag})
}
}
for _, args := range commandArgs {
t.Run(fmt.Sprintf("Testing Command %s", args[0]), func(t *testing.T) {
defer func() {
err := recover()
if err != nil {
t.Fatalf("got an panic error while running the command: %s -h. Error: %s", args[0], err)
}
}()
rootCmd.SetArgs(args[1:])
rootCmd.SetOut(io.Discard)
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running %s command, got %v", args[0], err)
return
}
})
}
}

View File

@@ -1,38 +1,21 @@
package cmd
import (
"context"
"runtime"
"github.com/kardianos/service"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"github.com/netbirdio/netbird/client/internal"
)
type program struct {
ctx context.Context
cancel context.CancelFunc
serv *grpc.Server
}
func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
ctx = internal.CtxInitState(ctx)
return &program{ctx: ctx, cancel: cancel}
cmd *cobra.Command
args []string
}
func newSVCConfig() *service.Config {
name := "netbird"
if runtime.GOOS == "windows" {
name = "Netbird"
}
return &service.Config{
Name: name,
DisplayName: "Netbird",
Name: "wiretrustee",
DisplayName: "Wiretrustee",
Description: "A WireGuard-based mesh network that connects your devices into a single private network.",
Option: make(service.KeyValue),
}
}
@@ -45,7 +28,9 @@ func newSVC(prg *program, conf *service.Config) (service.Service, error) {
return s, nil
}
var serviceCmd = &cobra.Command{
Use: "service",
Short: "manages Netbird service",
}
var (
serviceCmd = &cobra.Command{
Use: "service",
Short: "manages wiretrustee service",
}
)

View File

@@ -1,216 +1,152 @@
package cmd
import (
"context"
"fmt"
"net"
"os"
"strings"
"time"
"github.com/kardianos/service"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/server"
"github.com/netbirdio/netbird/util"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"github.com/wiretrustee/wiretrustee/util"
"time"
)
func (p *program) Start(svc service.Service) error {
func (p *program) Start(service.Service) error {
// Start should not block. Do the actual work async.
log.Info("starting Netbird service") //nolint
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
p.serv = grpc.NewServer()
split := strings.Split(daemonAddr, "://")
switch split[0] {
case "unix":
// cleanup failed close
stat, err := os.Stat(split[1])
if err == nil && !stat.IsDir() {
if err := os.Remove(split[1]); err != nil {
log.Debugf("remove socket file: %v", err)
}
}
case "tcp":
default:
return fmt.Errorf("unsupported daemon address protocol: %v", split[0])
}
listen, err := net.Listen(split[0], split[1])
if err != nil {
return fmt.Errorf("failed to listen daemon interface: %w", err)
}
log.Info("starting service") //nolint
go func() {
defer listen.Close()
if split[0] == "unix" {
err = os.Chmod(split[1], 0666)
if err != nil {
log.Errorf("failed setting daemon permissions: %v", split[1])
return
}
}
serverInstance := server.New(p.ctx, configPath, logFile)
if err := serverInstance.Start(); err != nil {
log.Fatalf("failed to start daemon: %v", err)
}
proto.RegisterDaemonServiceServer(p.serv, serverInstance)
log.Printf("started daemon server: %v", split[1])
if err := p.serv.Serve(listen); err != nil {
log.Errorf("failed to serve daemon requests: %v", err)
err := runClient()
if err != nil {
log.Errorf("stopped Wiretrustee client app due to error: %v", err)
return
}
}()
return nil
}
func (p *program) Stop(srv service.Service) error {
p.cancel()
func (p *program) Stop(service.Service) error {
go func() {
stopCh <- 1
}()
if p.serv != nil {
p.serv.Stop()
select {
case <-cleanupCh:
case <-time.After(time.Second * 10):
log.Warnf("failed waiting for service cleanup, terminating")
}
time.Sleep(time.Second * 2)
log.Info("stopped Netbird service") //nolint
log.Info("stopped Wiretrustee service") //nolint
return nil
}
var runCmd = &cobra.Command{
Use: "run",
Short: "runs Netbird as service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
var (
runCmd = &cobra.Command{
Use: "run",
Short: "runs wiretrustee as service",
Run: func(cmd *cobra.Command, args []string) {
SetFlagsFromEnvVars()
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, logFile)
if err != nil {
log.Errorf("failed initializing log %v", err)
return
}
err := handleRebrand(cmd)
if err != nil {
return err
}
SetupCloseHandler()
err = util.InitLog(logLevel, logFile)
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
prg := &program{
cmd: cmd,
args: args,
}
ctx, cancel := context.WithCancel(cmd.Context())
SetupCloseHandler(ctx, cancel)
s, err := newSVC(prg, newSVCConfig())
if err != nil {
cmd.PrintErrln(err)
return
}
err = s.Run()
if err != nil {
cmd.PrintErrln(err)
return
}
cmd.Printf("Wiretrustee service is running")
},
}
)
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
return err
}
err = s.Run()
if err != nil {
return err
}
cmd.Printf("Netbird service is running")
return nil
},
}
var (
startCmd = &cobra.Command{
Use: "start",
Short: "starts wiretrustee service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
var startCmd = &cobra.Command{
Use: "start",
Short: "starts Netbird service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
err := util.InitLog(logLevel, logFile)
if err != nil {
log.Errorf("failed initializing log %v", err)
return err
}
s, err := newSVC(&program{}, newSVCConfig())
if err != nil {
cmd.PrintErrln(err)
return err
}
err = s.Start()
if err != nil {
cmd.PrintErrln(err)
return err
}
cmd.Println("Wiretrustee service has been started")
return nil
},
}
)
cmd.SetOut(cmd.OutOrStdout())
var (
stopCmd = &cobra.Command{
Use: "stop",
Short: "stops wiretrustee service",
Run: func(cmd *cobra.Command, args []string) {
SetFlagsFromEnvVars()
err := handleRebrand(cmd)
if err != nil {
return err
}
err := util.InitLog(logLevel, logFile)
if err != nil {
log.Errorf("failed initializing log %v", err)
}
s, err := newSVC(&program{}, newSVCConfig())
if err != nil {
cmd.PrintErrln(err)
return
}
err = s.Stop()
if err != nil {
cmd.PrintErrln(err)
return
}
cmd.Println("Wiretrustee service has been stopped")
},
}
)
err = util.InitLog(logLevel, logFile)
if err != nil {
return err
}
var (
restartCmd = &cobra.Command{
Use: "restart",
Short: "restarts wiretrustee service",
Run: func(cmd *cobra.Command, args []string) {
SetFlagsFromEnvVars()
ctx, cancel := context.WithCancel(cmd.Context())
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
cmd.PrintErrln(err)
return err
}
err = s.Start()
if err != nil {
cmd.PrintErrln(err)
return err
}
cmd.Println("Netbird service has been started")
return nil
},
}
var stopCmd = &cobra.Command{
Use: "stop",
Short: "stops Netbird service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
err = util.InitLog(logLevel, logFile)
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx, cancel := context.WithCancel(cmd.Context())
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
return err
}
err = s.Stop()
if err != nil {
return err
}
cmd.Println("Netbird service has been stopped")
return nil
},
}
var restartCmd = &cobra.Command{
Use: "restart",
Short: "restarts Netbird service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
err = util.InitLog(logLevel, logFile)
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx, cancel := context.WithCancel(cmd.Context())
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
return err
}
err = s.Restart()
if err != nil {
return err
}
cmd.Println("Netbird service has been restarted")
return nil
},
}
err := util.InitLog(logLevel, logFile)
if err != nil {
log.Errorf("failed initializing log %v", err)
}
s, err := newSVC(&program{}, newSVCConfig())
if err != nil {
cmd.PrintErrln(err)
return
}
err = s.Restart()
if err != nil {
cmd.PrintErrln(err)
return
}
cmd.Println("Wiretrustee service has been restarted")
},
}
)

View File

@@ -1,112 +1,69 @@
package cmd
import (
"context"
"os"
"path/filepath"
"runtime"
"github.com/spf13/cobra"
"runtime"
)
var installCmd = &cobra.Command{
Use: "install",
Short: "installs Netbird service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
var (
installCmd = &cobra.Command{
Use: "install",
Short: "installs wiretrustee service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
cmd.SetOut(cmd.OutOrStdout())
svcConfig := newSVCConfig()
err := handleRebrand(cmd)
if err != nil {
return err
}
svcConfig := newSVCConfig()
svcConfig.Arguments = []string{
"service",
"run",
"--config",
configPath,
"--log-level",
logLevel,
}
if managementURL != "" {
svcConfig.Arguments = append(svcConfig.Arguments, "--management-url", managementURL)
}
if logFile != "console" {
svcConfig.Arguments = append(svcConfig.Arguments, "--log-file", logFile)
}
if runtime.GOOS == "linux" {
// Respected only by systemd systems
svcConfig.Dependencies = []string{"After=network.target syslog.target"}
if logFile != "console" {
setStdLogPath := true
dir := filepath.Dir(logFile)
_, err := os.Stat(dir)
if err != nil {
err = os.MkdirAll(dir, 0750)
if err != nil {
setStdLogPath = false
}
}
if setStdLogPath {
svcConfig.Option["LogOutput"] = true
svcConfig.Option["LogDirectory"] = dir
}
svcConfig.Arguments = []string{
"service",
"run",
"--config",
configPath,
"--log-level",
logLevel,
}
}
ctx, cancel := context.WithCancel(cmd.Context())
if runtime.GOOS == "linux" {
// Respected only by systemd systems
svcConfig.Dependencies = []string{"After=network.target syslog.target"}
}
s, err := newSVC(newProgram(ctx, cancel), svcConfig)
if err != nil {
cmd.PrintErrln(err)
return err
}
s, err := newSVC(&program{}, svcConfig)
if err != nil {
cmd.PrintErrln(err)
return err
}
err = s.Install()
if err != nil {
cmd.PrintErrln(err)
return err
}
cmd.Println("Netbird service has been installed")
return nil
},
}
err = s.Install()
if err != nil {
cmd.PrintErrln(err)
return err
}
cmd.Println("Wiretrustee service has been installed")
return nil
},
}
)
var uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "uninstalls Netbird service from system",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
var (
uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "uninstalls wiretrustee service from system",
Run: func(cmd *cobra.Command, args []string) {
SetFlagsFromEnvVars()
cmd.SetOut(cmd.OutOrStdout())
s, err := newSVC(&program{}, newSVCConfig())
if err != nil {
cmd.PrintErrln(err)
return
}
err := handleRebrand(cmd)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(cmd.Context())
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
return err
}
err = s.Uninstall()
if err != nil {
return err
}
cmd.Println("Netbird has been uninstalled")
return nil
},
}
err = s.Uninstall()
if err != nil {
cmd.PrintErrln(err)
return
}
cmd.Println("Wiretrustee has been uninstalled")
},
}
)

View File

@@ -1,120 +0,0 @@
package cmd
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal"
nbssh "github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/util"
)
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(rootCmd)
SetFlagsFromEnvVars(cmd)
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.UpdateConfig(internal.ConfigInput{
ConfigPath: configPath,
})
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))
}

View File

@@ -1,435 +0,0 @@
package cmd
import (
"context"
"encoding/json"
"fmt"
"net"
"net/netip"
"sort"
"strings"
"time"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
"gopkg.in/yaml.v3"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/util"
"github.com/netbirdio/netbird/version"
)
type peerStateDetailOutput struct {
FQDN string `json:"fqdn" yaml:"fqdn"`
IP string `json:"netbirdIp" yaml:"netbirdIp"`
PubKey string `json:"publicKey" yaml:"publicKey"`
Status string `json:"status" yaml:"status"`
LastStatusUpdate time.Time `json:"lastStatusUpdate" yaml:"lastStatusUpdate"`
ConnType string `json:"connectionType" yaml:"connectionType"`
Direct bool `json:"direct" yaml:"direct"`
IceCandidateType iceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"`
}
type peersStateOutput struct {
Total int `json:"total" yaml:"total"`
Connected int `json:"connected" yaml:"connected"`
Details []peerStateDetailOutput `json:"details" yaml:"details"`
}
type signalStateOutput struct {
URL string `json:"url" yaml:"url"`
Connected bool `json:"connected" yaml:"connected"`
}
type managementStateOutput struct {
URL string `json:"url" yaml:"url"`
Connected bool `json:"connected" yaml:"connected"`
}
type iceCandidateType struct {
Local string `json:"local" yaml:"local"`
Remote string `json:"remote" yaml:"remote"`
}
type statusOutputOverview struct {
Peers peersStateOutput `json:"peers" yaml:"peers"`
CliVersion string `json:"cliVersion" yaml:"cliVersion"`
DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"`
ManagementState managementStateOutput `json:"management" yaml:"management"`
SignalState signalStateOutput `json:"signal" yaml:"signal"`
IP string `json:"netbirdIp" yaml:"netbirdIp"`
PubKey string `json:"publicKey" yaml:"publicKey"`
KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"`
FQDN string `json:"fqdn" yaml:"fqdn"`
}
var (
detailFlag bool
ipv4Flag bool
jsonFlag bool
yamlFlag bool
ipsFilter []string
statusFilter string
ipsFilterMap map[string]struct{}
)
var statusCmd = &cobra.Command{
Use: "status",
Short: "status of the Netbird Service",
RunE: statusFunc,
}
func init() {
ipsFilterMap = make(map[string]struct{})
statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information in human-readable format")
statusCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "display detailed status information in json format")
statusCmd.PersistentFlags().BoolVar(&yamlFlag, "yaml", false, "display detailed status information in yaml format")
statusCmd.PersistentFlags().BoolVar(&ipv4Flag, "ipv4", false, "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33")
statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4")
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 statusFunc(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := parseFilters()
if err != nil {
return err
}
err = util.InitLog(logLevel, "console")
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx := internal.CtxInitState(context.Background())
resp, _ := getStatus(ctx, cmd)
if err != nil {
return nil
}
if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) {
cmd.Printf("Daemon status: %s\n\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",
resp.GetStatus(),
)
return nil
}
if ipv4Flag {
cmd.Print(parseInterfaceIP(resp.GetFullStatus().GetLocalPeerState().GetIP()))
return nil
}
outputInformationHolder := convertToStatusOutputOverview(resp)
statusOutputString := ""
switch {
case detailFlag:
statusOutputString = parseToFullDetailSummary(outputInformationHolder)
case jsonFlag:
statusOutputString, err = parseToJSON(outputInformationHolder)
case yamlFlag:
statusOutputString, err = parseToYAML(outputInformationHolder)
default:
statusOutputString = parseGeneralSummary(outputInformationHolder, false)
}
if err != nil {
return err
}
cmd.Print(statusOutputString)
return nil
}
func getStatus(ctx context.Context, cmd *cobra.Command) (*proto.StatusResponse, error) {
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
return nil, fmt.Errorf("failed to connect to daemon error: %v\n"+
"If the daemon is not running please run: "+
"\nnetbird service install \nnetbird service start\n", err)
}
defer conn.Close()
resp, err := proto.NewDaemonServiceClient(conn).Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true})
if err != nil {
return nil, fmt.Errorf("status failed: %v", status.Convert(err).Message())
}
return resp, nil
}
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 convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverview {
pbFullStatus := resp.GetFullStatus()
managementState := pbFullStatus.GetManagementState()
managementOverview := managementStateOutput{
URL: managementState.GetURL(),
Connected: managementState.GetConnected(),
}
signalState := pbFullStatus.GetSignalState()
signalOverview := signalStateOutput{
URL: signalState.GetURL(),
Connected: signalState.GetConnected(),
}
peersOverview := mapPeers(resp.GetFullStatus().GetPeers())
overview := statusOutputOverview{
Peers: peersOverview,
CliVersion: version.NetbirdVersion(),
DaemonVersion: resp.GetDaemonVersion(),
ManagementState: managementOverview,
SignalState: signalOverview,
IP: pbFullStatus.GetLocalPeerState().GetIP(),
PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(),
KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(),
FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(),
}
return overview
}
func mapPeers(peers []*proto.PeerState) peersStateOutput {
var peersStateDetail []peerStateDetailOutput
localICE := ""
remoteICE := ""
connType := ""
peersConnected := 0
for _, pbPeerState := range peers {
isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String()
if skipDetailByFilters(pbPeerState, isPeerConnected) {
continue
}
if isPeerConnected {
peersConnected = peersConnected + 1
localICE = pbPeerState.GetLocalIceCandidateType()
remoteICE = pbPeerState.GetRemoteIceCandidateType()
connType = "P2P"
if pbPeerState.Relayed {
connType = "Relayed"
}
}
timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local()
peerState := peerStateDetailOutput{
IP: pbPeerState.GetIP(),
PubKey: pbPeerState.GetPubKey(),
Status: pbPeerState.GetConnStatus(),
LastStatusUpdate: timeLocal.UTC(),
ConnType: connType,
Direct: pbPeerState.GetDirect(),
IceCandidateType: iceCandidateType{
Local: localICE,
Remote: remoteICE,
},
FQDN: pbPeerState.GetFqdn(),
}
peersStateDetail = append(peersStateDetail, peerState)
}
sortPeersByIP(peersStateDetail)
peersOverview := peersStateOutput{
Total: len(peersStateDetail),
Connected: peersConnected,
Details: peersStateDetail,
}
return peersOverview
}
func sortPeersByIP(peersStateDetail []peerStateDetailOutput) {
if len(peersStateDetail) > 0 {
sort.SliceStable(peersStateDetail, func(i, j int) bool {
iAddr, _ := netip.ParseAddr(peersStateDetail[i].IP)
jAddr, _ := netip.ParseAddr(peersStateDetail[j].IP)
return iAddr.Compare(jAddr) == -1
})
}
}
func parseInterfaceIP(interfaceIP string) string {
ip, _, err := net.ParseCIDR(interfaceIP)
if err != nil {
return ""
}
return fmt.Sprintf("%s\n", ip)
}
func parseToJSON(overview statusOutputOverview) (string, error) {
jsonBytes, err := json.Marshal(overview)
if err != nil {
return "", fmt.Errorf("json marshal failed")
}
return string(jsonBytes), err
}
func parseToYAML(overview statusOutputOverview) (string, error) {
yamlBytes, err := yaml.Marshal(overview)
if err != nil {
return "", fmt.Errorf("yaml marshal failed")
}
return string(yamlBytes), nil
}
func parseGeneralSummary(overview statusOutputOverview, showURL bool) string {
managementConnString := "Disconnected"
if overview.ManagementState.Connected {
managementConnString = "Connected"
if showURL {
managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL)
}
}
signalConnString := "Disconnected"
if overview.SignalState.Connected {
signalConnString = "Connected"
if showURL {
signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL)
}
}
interfaceTypeString := "Userspace"
interfaceIP := overview.IP
if overview.KernelInterface {
interfaceTypeString = "Kernel"
} else if overview.IP == "" {
interfaceTypeString = "N/A"
interfaceIP = "N/A"
}
peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total)
summary := fmt.Sprintf(
"Daemon version: %s\n"+
"CLI version: %s\n"+
"Management: %s\n"+
"Signal: %s\n"+
"FQDN: %s\n"+
"NetBird IP: %s\n"+
"Interface type: %s\n"+
"Peers count: %s\n",
overview.DaemonVersion,
version.NetbirdVersion(),
managementConnString,
signalConnString,
overview.FQDN,
interfaceIP,
interfaceTypeString,
peersCountString,
)
return summary
}
func parseToFullDetailSummary(overview statusOutputOverview) string {
parsedPeersString := parsePeers(overview.Peers)
summary := parseGeneralSummary(overview, true)
return fmt.Sprintf(
"Peers detail:"+
"%s\n"+
"%s",
parsedPeersString,
summary,
)
}
func parsePeers(peers peersStateOutput) string {
var (
peersString = ""
)
for _, peerState := range peers.Details {
localICE := "-"
if peerState.IceCandidateType.Local != "" {
localICE = peerState.IceCandidateType.Local
}
remoteICE := "-"
if peerState.IceCandidateType.Remote != "" {
remoteICE = peerState.IceCandidateType.Remote
}
peerString := fmt.Sprintf(
"\n %s:\n"+
" NetBird IP: %s\n"+
" Public key: %s\n"+
" Status: %s\n"+
" -- detail --\n"+
" Connection type: %s\n"+
" Direct: %t\n"+
" ICE candidate (Local/Remote): %s/%s\n"+
" Last connection update: %s\n",
peerState.FQDN,
peerState.IP,
peerState.PubKey,
peerState.Status,
peerState.ConnType,
peerState.Direct,
localICE,
remoteICE,
peerState.LastStatusUpdate.Format("2006-01-02 15:04:05"),
)
peersString = peersString + peerString
}
return peersString
}
func skipDetailByFilters(peerState *proto.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
}

View File

@@ -1,301 +0,0 @@
package cmd
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/version"
)
var resp = &proto.StatusResponse{
Status: "Connected",
FullStatus: &proto.FullStatus{
Peers: []*proto.PeerState{
{
IP: "192.168.178.101",
PubKey: "Pubkey1",
Fqdn: "peer-1.awesome-domain.com",
ConnStatus: "Connected",
ConnStatusUpdate: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 1, 0, time.UTC)),
Relayed: false,
Direct: true,
LocalIceCandidateType: "",
RemoteIceCandidateType: "",
},
{
IP: "192.168.178.102",
PubKey: "Pubkey2",
Fqdn: "peer-2.awesome-domain.com",
ConnStatus: "Connected",
ConnStatusUpdate: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 2, 0, time.UTC)),
Relayed: true,
Direct: false,
LocalIceCandidateType: "relay",
RemoteIceCandidateType: "prflx",
},
},
ManagementState: &proto.ManagementState{
URL: "my-awesome-management.com:443",
Connected: true,
},
SignalState: &proto.SignalState{
URL: "my-awesome-signal.com:443",
Connected: true,
},
LocalPeerState: &proto.LocalPeerState{
IP: "192.168.178.100/16",
PubKey: "Some-Pub-Key",
KernelInterface: true,
Fqdn: "some-localhost.awesome-domain.com",
},
},
DaemonVersion: "0.14.1",
}
var overview = statusOutputOverview{
Peers: peersStateOutput{
Total: 2,
Connected: 2,
Details: []peerStateDetailOutput{
{
IP: "192.168.178.101",
PubKey: "Pubkey1",
FQDN: "peer-1.awesome-domain.com",
Status: "Connected",
LastStatusUpdate: time.Date(2001, 1, 1, 1, 1, 1, 0, time.UTC),
ConnType: "P2P",
Direct: true,
IceCandidateType: iceCandidateType{
Local: "",
Remote: "",
},
},
{
IP: "192.168.178.102",
PubKey: "Pubkey2",
FQDN: "peer-2.awesome-domain.com",
Status: "Connected",
LastStatusUpdate: time.Date(2002, 2, 2, 2, 2, 2, 0, time.UTC),
ConnType: "Relayed",
Direct: false,
IceCandidateType: iceCandidateType{
Local: "relay",
Remote: "prflx",
},
},
},
},
CliVersion: version.NetbirdVersion(),
DaemonVersion: "0.14.1",
ManagementState: managementStateOutput{
URL: "my-awesome-management.com:443",
Connected: true,
},
SignalState: signalStateOutput{
URL: "my-awesome-signal.com:443",
Connected: true,
},
IP: "192.168.178.100/16",
PubKey: "Some-Pub-Key",
KernelInterface: true,
FQDN: "some-localhost.awesome-domain.com",
}
func TestConversionFromFullStatusToOutputOverview(t *testing.T) {
convertedResult := convertToStatusOutputOverview(resp)
assert.Equal(t, overview, convertedResult)
}
func TestSortingOfPeers(t *testing.T) {
peers := []peerStateDetailOutput{
{
IP: "192.168.178.104",
},
{
IP: "192.168.178.102",
},
{
IP: "192.168.178.101",
},
{
IP: "192.168.178.105",
},
{
IP: "192.168.178.103",
},
}
sortPeersByIP(peers)
assert.Equal(t, peers[3].IP, "192.168.178.104")
}
func TestParsingToJSON(t *testing.T) {
json, _ := parseToJSON(overview)
//@formatter:off
expectedJSON := "{\"" +
"peers\":" +
"{" +
"\"total\":2," +
"\"connected\":2," +
"\"details\":" +
"[" +
"{" +
"\"fqdn\":\"peer-1.awesome-domain.com\"," +
"\"netbirdIp\":\"192.168.178.101\"," +
"\"publicKey\":\"Pubkey1\"," +
"\"status\":\"Connected\"," +
"\"lastStatusUpdate\":\"2001-01-01T01:01:01Z\"," +
"\"connectionType\":\"P2P\"," +
"\"direct\":true," +
"\"iceCandidateType\":" +
"{" +
"\"local\":\"\"," +
"\"remote\":\"\"" +
"}" +
"}," +
"{" +
"\"fqdn\":\"peer-2.awesome-domain.com\"," +
"\"netbirdIp\":\"192.168.178.102\"," +
"\"publicKey\":\"Pubkey2\"," +
"\"status\":\"Connected\"," +
"\"lastStatusUpdate\":\"2002-02-02T02:02:02Z\"," +
"\"connectionType\":\"Relayed\"," +
"\"direct\":false," +
"\"iceCandidateType\":" +
"{" +
"\"local\":\"relay\"," +
"\"remote\":\"prflx\"" +
"}" +
"}" +
"]" +
"}," +
"\"cliVersion\":\"development\"," +
"\"daemonVersion\":\"0.14.1\"," +
"\"management\":" +
"{" +
"\"url\":\"my-awesome-management.com:443\"," +
"\"connected\":true" +
"}," +
"\"signal\":" +
"{\"" +
"url\":\"my-awesome-signal.com:443\"," +
"\"connected\":true" +
"}," +
"\"netbirdIp\":\"192.168.178.100/16\"," +
"\"publicKey\":\"Some-Pub-Key\"," +
"\"usesKernelInterface\":true," +
"\"fqdn\":\"some-localhost.awesome-domain.com\"" +
"}"
// @formatter:on
assert.Equal(t, expectedJSON, json)
}
func TestParsingToYAML(t *testing.T) {
yaml, _ := parseToYAML(overview)
expectedYAML := "peers:\n" +
" total: 2\n" +
" connected: 2\n" +
" details:\n" +
" - fqdn: peer-1.awesome-domain.com\n" +
" netbirdIp: 192.168.178.101\n" +
" publicKey: Pubkey1\n" +
" status: Connected\n" +
" lastStatusUpdate: 2001-01-01T01:01:01Z\n" +
" connectionType: P2P\n" +
" direct: true\n" +
" iceCandidateType:\n" +
" local: \"\"\n" +
" remote: \"\"\n" +
" - fqdn: peer-2.awesome-domain.com\n" +
" netbirdIp: 192.168.178.102\n" +
" publicKey: Pubkey2\n" +
" status: Connected\n" +
" lastStatusUpdate: 2002-02-02T02:02:02Z\n" +
" connectionType: Relayed\n" +
" direct: false\n" +
" iceCandidateType:\n" +
" local: relay\n" +
" remote: prflx\n" +
"cliVersion: development\n" +
"daemonVersion: 0.14.1\n" +
"management:\n" +
" url: my-awesome-management.com:443\n" +
" connected: true\n" +
"signal:\n" +
" url: my-awesome-signal.com:443\n" +
" connected: true\n" +
"netbirdIp: 192.168.178.100/16\n" +
"publicKey: Some-Pub-Key\n" +
"usesKernelInterface: true\n" +
"fqdn: some-localhost.awesome-domain.com\n"
assert.Equal(t, expectedYAML, yaml)
}
func TestParsingToDetail(t *testing.T) {
detail := parseToFullDetailSummary(overview)
expectedDetail := "Peers detail:\n" +
" peer-1.awesome-domain.com:\n" +
" NetBird IP: 192.168.178.101\n" +
" Public key: Pubkey1\n" +
" Status: Connected\n" +
" -- detail --\n" +
" Connection type: P2P\n" +
" Direct: true\n" +
" ICE candidate (Local/Remote): -/-\n" +
" Last connection update: 2001-01-01 01:01:01\n" +
"\n" +
" peer-2.awesome-domain.com:\n" +
" NetBird IP: 192.168.178.102\n" +
" Public key: Pubkey2\n" +
" Status: Connected\n" +
" -- detail --\n" +
" Connection type: Relayed\n" +
" Direct: false\n" +
" ICE candidate (Local/Remote): relay/prflx\n" +
" Last connection update: 2002-02-02 02:02:02\n" +
"\n" +
"Daemon version: 0.14.1\n" +
"CLI version: development\n" +
"Management: Connected to my-awesome-management.com:443\n" +
"Signal: Connected to my-awesome-signal.com:443\n" +
"FQDN: some-localhost.awesome-domain.com\n" +
"NetBird IP: 192.168.178.100/16\n" +
"Interface type: Kernel\n" +
"Peers count: 2/2 Connected\n"
assert.Equal(t, expectedDetail, detail)
}
func TestParsingToShortVersion(t *testing.T) {
shortVersion := parseGeneralSummary(overview, false)
expectedString := "Daemon version: 0.14.1\n" +
"CLI version: development\n" +
"Management: Connected\n" +
"Signal: Connected\n" +
"FQDN: some-localhost.awesome-domain.com\n" +
"NetBird IP: 192.168.178.100/16\n" +
"Interface type: Kernel\n" +
"Peers count: 2/2 Connected\n"
assert.Equal(t, expectedString, shortVersion)
}
func TestParsingOfIP(t *testing.T) {
InterfaceIP := "192.168.178.123/16"
parsedIP := parseInterfaceIP(InterfaceIP)
assert.Equal(t, "192.168.178.123\n", parsedIP)
}

View File

@@ -1,46 +1,15 @@
package cmd
import (
"context"
"github.com/netbirdio/netbird/management/server/activity"
"net"
"path/filepath"
"testing"
"time"
"github.com/netbirdio/netbird/util"
clientProto "github.com/netbirdio/netbird/client/proto"
client "github.com/netbirdio/netbird/client/server"
mgmtProto "github.com/netbirdio/netbird/management/proto"
mgmt "github.com/netbirdio/netbird/management/server"
sigProto "github.com/netbirdio/netbird/signal/proto"
sig "github.com/netbirdio/netbird/signal/server"
mgmtProto "github.com/wiretrustee/wiretrustee/management/proto"
mgmt "github.com/wiretrustee/wiretrustee/management/server"
sigProto "github.com/wiretrustee/wiretrustee/signal/proto"
sig "github.com/wiretrustee/wiretrustee/signal/server"
"google.golang.org/grpc"
"net"
"testing"
)
func startTestingServices(t *testing.T) string {
config := &mgmt.Config{}
_, err := util.ReadJson("../testdata/management.json", config)
if err != nil {
t.Fatal(err)
}
testDir := t.TempDir()
config.Datadir = testDir
err = util.CopyFileContents("../testdata/store.json", filepath.Join(testDir, "store.json"))
if err != nil {
t.Fatal(err)
}
_, signalLis := startSignal(t)
signalAddr := signalLis.Addr().String()
config.Signal.URI = signalAddr
_, mgmLis := startManagement(t, config)
mgmAddr := mgmLis.Addr().String()
return mgmAddr
}
func startSignal(t *testing.T) (*grpc.Server, net.Listener) {
lis, err := net.Listen("tcp", ":0")
if err != nil {
@@ -57,29 +26,21 @@ func startSignal(t *testing.T) (*grpc.Server, net.Listener) {
return s, lis
}
func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Listener) {
func startManagement(config *mgmt.Config, t *testing.T) (*grpc.Server, net.Listener) {
lis, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
s := grpc.NewServer()
store, err := mgmt.NewFileStore(config.Datadir)
store, err := mgmt.NewStore(config.Datadir)
if err != nil {
t.Fatal(err)
}
peersUpdateManager := mgmt.NewPeersUpdateManager()
eventStore := &activity.InMemoryEventStore{}
if err != nil {
return nil, nil
}
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "",
eventStore)
if err != nil {
t.Fatal(err)
}
accountManager := mgmt.NewManager(store, peersUpdateManager)
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager, nil)
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager)
if err != nil {
t.Fatal(err)
}
@@ -87,34 +48,9 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste
go func() {
if err := s.Serve(lis); err != nil {
t.Error(err)
return
}
}()
return s, lis
}
func startClientDaemon(
t *testing.T, ctx context.Context, managementURL, configPath string,
) (*grpc.Server, net.Listener) {
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
s := grpc.NewServer()
server := client.New(ctx,
configPath, "")
if err := server.Start(); err != nil {
t.Fatal(err)
}
clientProto.RegisterDaemonServiceServer(s, server)
go func() {
if err := s.Serve(lis); err != nil {
t.Error(err)
}
}()
time.Sleep(time.Second)
return s, lis
}

View File

@@ -2,267 +2,234 @@ package cmd
import (
"context"
"fmt"
"net"
"net/netip"
"strings"
"github.com/cenkalti/backoff/v4"
"github.com/kardianos/service"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/wiretrustee/wiretrustee/client/internal"
mgm "github.com/wiretrustee/wiretrustee/management/client"
mgmProto "github.com/wiretrustee/wiretrustee/management/proto"
signal "github.com/wiretrustee/wiretrustee/signal/client"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/util"
)
const (
invalidInputType int = iota
ipInputType
interfaceInputType
"google.golang.org/grpc/status"
"time"
)
var (
foregroundMode bool
upCmd = &cobra.Command{
upCmd = &cobra.Command{
Use: "up",
Short: "install, login and start Netbird client",
RunE: upFunc,
Short: "install, login and start wiretrustee client",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
err := loginCmd.RunE(cmd, args)
if err != nil {
return err
}
if logFile == "console" {
return runClient()
}
s, err := newSVC(&program{}, newSVCConfig())
if err != nil {
cmd.PrintErrln(err)
return err
}
srvStatus, err := s.Status()
if err != nil {
if err == service.ErrNotInstalled {
log.Infof("%s. Installing it now", err.Error())
e := installCmd.RunE(cmd, args)
if e != nil {
return e
}
} else {
log.Warnf("failed retrieving service status: %v", err)
}
}
if srvStatus == service.StatusRunning {
stopCmd.Run(cmd, args)
}
return startCmd.RunE(cmd, args)
},
}
)
func init() {
upCmd.PersistentFlags().BoolVarP(&foregroundMode, "foreground-mode", "F", false, "start service in foreground")
}
func upFunc(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
SetFlagsFromEnvVars(cmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
// createEngineConfig converts configuration received from Management Service to EngineConfig
func createEngineConfig(key wgtypes.Key, config *internal.Config, peerConfig *mgmProto.PeerConfig) (*internal.EngineConfig, error) {
iFaceBlackList := make(map[string]struct{})
for i := 0; i < len(config.IFaceBlackList); i += 2 {
iFaceBlackList[config.IFaceBlackList[i]] = struct{}{}
}
err = validateNATExternalIPs(natExternalIPs)
if err != nil {
return err
engineConf := &internal.EngineConfig{
WgIface: config.WgIface,
WgAddr: peerConfig.Address,
IFaceBlackList: iFaceBlackList,
WgPrivateKey: key,
}
ctx := internal.CtxInitState(cmd.Context())
if foregroundMode {
return runInForegroundMode(ctx, cmd)
}
return runInDaemonMode(ctx, cmd)
}
func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
err := handleRebrand(cmd)
if err != nil {
return err
}
customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
if err != nil {
return err
}
config, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: configPath,
PreSharedKey: &preSharedKey,
NATExternalIPs: natExternalIPs,
CustomDNSAddress: customDNSAddressConverted,
})
if err != nil {
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)
}
var cancel context.CancelFunc
ctx, cancel = context.WithCancel(ctx)
SetupCloseHandler(ctx, cancel)
return internal.RunClient(ctx, config, peer.NewRecorder(config.ManagementURL.String()), nil, nil)
}
func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
if err != nil {
return err
}
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
return fmt.Errorf("failed to connect to daemon error: %v\n"+
"If the daemon is not running please run: "+
"\nnetbird service install \nnetbird service start\n", err)
}
defer func() {
err := conn.Close()
if config.PreSharedKey != "" {
preSharedKey, err := wgtypes.ParseKey(config.PreSharedKey)
if err != nil {
log.Warnf("failed closing dameon gRPC client connection %v", err)
return
return nil, err
}
}()
engineConf.PreSharedKey = &preSharedKey
}
client := proto.NewDaemonServiceClient(conn)
return engineConf, nil
}
status, err := client.Status(ctx, &proto.StatusRequest{})
// connectToSignal creates Signal Service client and established a connection
func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig, ourPrivateKey wgtypes.Key) (*signal.Client, error) {
var sigTLSEnabled bool
if wtConfig.Signal.Protocol == mgmProto.HostConfig_HTTPS {
sigTLSEnabled = true
} else {
sigTLSEnabled = false
}
signalClient, err := signal.NewClient(ctx, wtConfig.Signal.Uri, ourPrivateKey, sigTLSEnabled)
if err != nil {
return fmt.Errorf("unable to get daemon status: %v", err)
log.Errorf("error while connecting to the Signal Exchange Service %s: %s", wtConfig.Signal.Uri, err)
return nil, status.Errorf(codes.FailedPrecondition, "failed connecting to Signal Service : %s", err)
}
if status.Status == string(internal.StatusConnected) {
cmd.Println("Already connected")
return nil
}
return signalClient, nil
}
loginRequest := proto.LoginRequest{
SetupKey: setupKey,
PreSharedKey: preSharedKey,
ManagementUrl: managementURL,
AdminURL: adminURL,
NatExternalIPs: natExternalIPs,
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
CustomDNSAddress: customDNSAddressConverted,
}
var loginErr error
var loginResp *proto.LoginResponse
err = WithBackOff(func() error {
var backOffErr error
loginResp, backOffErr = client.Login(ctx, &loginRequest)
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
s.Code() == codes.PermissionDenied ||
s.Code() == codes.NotFound ||
s.Code() == codes.Unimplemented) {
loginErr = backOffErr
return nil
}
return backOffErr
})
// 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.Client, *mgmProto.LoginResponse, error) {
log.Debugf("connecting to management server %s", managementAddr)
client, err := mgm.NewClient(ctx, managementAddr, ourPrivateKey, tlsEnabled)
if err != nil {
return fmt.Errorf("login backoff cycle failed: %v", err)
return nil, nil, status.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err)
}
log.Debugf("connected to management server %s", managementAddr)
if loginErr != nil {
return fmt.Errorf("login failed: %v", loginErr)
}
if loginResp.NeedsSSOLogin {
openURL(cmd, loginResp.VerificationURIComplete)
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
if err != nil {
return fmt.Errorf("waiting sso login failed with: %v", err)
}
}
if _, err := client.Up(ctx, &proto.UpRequest{}); err != nil {
return fmt.Errorf("call service up method: %v", err)
}
cmd.Println("Connected")
return nil
}
func validateNATExternalIPs(list []string) error {
for _, element := range list {
if element == "" {
return fmt.Errorf("empty string is not a valid input for %s", externalIPMapFlag)
}
subElements := strings.Split(element, "/")
if len(subElements) > 2 {
return fmt.Errorf("%s is not a valid input for %s. it should be formated as \"String\" or \"String/String\"", element, externalIPMapFlag)
}
if len(subElements) == 1 && !isValidIP(subElements[0]) {
return fmt.Errorf("%s is not a valid input for %s. it should be formated as \"IP\" or \"IP/IP\", or \"IP/Interface Name\"", element, externalIPMapFlag)
}
last := 0
for _, singleElement := range subElements {
inputType, err := validateElement(singleElement)
if err != nil {
return fmt.Errorf("%s is not a valid input for %s. it should be an IP string or a network name", singleElement, externalIPMapFlag)
}
if last == interfaceInputType && inputType == interfaceInputType {
return fmt.Errorf("%s is not a valid input for %s. it should not contain two interface names", element, externalIPMapFlag)
}
last = inputType
}
}
return nil
}
func validateElement(element string) (int, error) {
if isValidIP(element) {
return ipInputType, nil
}
validIface, err := isValidInterface(element)
serverPublicKey, err := client.GetServerPublicKey()
if err != nil {
return invalidInputType, fmt.Errorf("unable to validate the network interface name, error: %s", err)
return nil, nil, status.Errorf(codes.FailedPrecondition, "failed while getting Management Service public key: %s", err)
}
if validIface {
return interfaceInputType, nil
}
return interfaceInputType, fmt.Errorf("invalid IP or network interface name not found")
}
func isValidIP(ip string) bool {
return net.ParseIP(ip) != nil
}
func isValidInterface(name string) (bool, error) {
netInterfaces, err := net.Interfaces()
loginResp, err := client.Login(*serverPublicKey)
if err != nil {
return false, err
}
for _, iface := range netInterfaces {
if iface.Name == name {
return true, nil
}
}
return false, nil
}
func parseCustomDNSAddress(modified bool) ([]byte, error) {
var parsed []byte
if modified {
if !isValidAddrPort(customDNSAddress) {
return nil, fmt.Errorf("%s is invalid, it should be formated as IP:Port string or as an empty string like \"\"", customDNSAddress)
}
if customDNSAddress == "" && logFile != "console" {
parsed = []byte("empty")
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
log.Error("peer registration required. Please run wiretrustee login command first")
return nil, nil, err
} else {
parsed = []byte(customDNSAddress)
return nil, nil, err
}
}
return parsed, nil
log.Debugf("peer logged in to Management Service %s", managementAddr)
return client, loginResp, nil
}
func isValidAddrPort(input string) bool {
if input == "" {
return true
func runClient() error {
var 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)
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}
_, err := netip.ParseAddrPort(input)
return err == nil
operation := func() error {
config, err := internal.ReadConfig(managementURL, configPath)
if err != nil {
log.Errorf("failed reading config %s %v", configPath, err)
return err
}
//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 err
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mgmTlsEnabled := false
if config.ManagementURL.Scheme == "https" {
mgmTlsEnabled = true
}
// connect (just a connection, no stream yet) and login to Management Service to get an initial global Wiretrustee config
mgmClient, loginResp, err := connectToManagement(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
if err != nil {
log.Warn(err)
return err
}
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
signalClient, err := connectToSignal(ctx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
if err != nil {
log.Error(err)
return err
}
peerConfig := loginResp.GetPeerConfig()
engineConfig, err := createEngineConfig(myPrivateKey, config, peerConfig)
if err != nil {
log.Error(err)
return err
}
// create start the Wiretrustee Engine that will connect to the Signal and Management streams and manage connections to remote peers.
engine := internal.NewEngine(signalClient, mgmClient, engineConfig, cancel, ctx)
err = engine.Start()
if err != nil {
log.Errorf("error while starting Wiretrustee Connection Engine: %s", err)
return err
}
log.Print("Wiretrustee engine started, my IP is: ", peerConfig.Address)
select {
case <-stopCh:
case <-ctx.Done():
}
backOff.Reset()
err = mgmClient.Close()
if err != nil {
log.Errorf("failed closing Management Service client %v", err)
return err
}
err = signalClient.Close()
if err != nil {
log.Errorf("failed closing Signal Service client %v", err)
return err
}
err = engine.Stop()
if err != nil {
log.Errorf("failed stopping engine %v", err)
return err
}
go func() {
cleanupCh <- struct{}{}
}()
log.Info("stopped Wiretrustee client")
return ctx.Err()
}
err := backoff.Retry(operation, backOff)
if err != nil {
log.Errorf("exiting client retry loop due to unrecoverable error: %s", err)
return err
}
return nil
}

View File

@@ -1,75 +0,0 @@
package cmd
import (
"context"
"testing"
"time"
"github.com/netbirdio/netbird/client/internal"
)
var cliAddr string
func TestUpDaemon(t *testing.T) {
mgmAddr := startTestingServices(t)
tempDir := t.TempDir()
confPath := tempDir + "/config.json"
ctx := internal.CtxInitState(context.Background())
state := internal.CtxGetState(ctx)
_, cliLis := startClientDaemon(t, ctx, "http://"+mgmAddr, confPath)
cliAddr = cliLis.Addr().String()
daemonAddr = "tcp://" + cliAddr
rootCmd.SetArgs([]string{
"login",
"--daemon-addr", "tcp://" + cliAddr,
"--setup-key", "A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
"--log-file", "",
})
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
return
}
time.Sleep(time.Second * 3)
if status, err := state.Status(); err != nil && status != internal.StatusIdle {
t.Errorf("wrong status after login: %s, %v", internal.StatusIdle, err)
return
}
rootCmd.SetArgs([]string{
"up",
"--daemon-addr", "tcp://" + cliAddr,
"--log-file", "",
})
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
return
}
time.Sleep(time.Second * 3)
if status, err := state.Status(); err != nil && status != internal.StatusConnected {
t.Errorf("wrong status after connect: %s, %v", status, err)
return
}
rootCmd.SetArgs([]string{
"status",
"--daemon-addr", "tcp://" + cliAddr,
})
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
return
}
time.Sleep(time.Second * 3)
rootCmd.SetErr(nil)
rootCmd.SetArgs([]string{"down", "--daemon-addr", "tcp://" + cliAddr})
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
return
}
// we can't check status here, because context already canceled
}

82
client/cmd/up_test.go Normal file
View File

@@ -0,0 +1,82 @@
package cmd
import (
"github.com/wiretrustee/wiretrustee/iface"
mgmt "github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/util"
"net/url"
"path/filepath"
"testing"
"time"
)
var signalAddr string
func TestUp_Start(t *testing.T) {
config := &mgmt.Config{}
_, err := util.ReadJson("../testdata/management.json", config)
if err != nil {
t.Fatal(err)
}
testDir := t.TempDir()
config.Datadir = testDir
err = util.CopyFileContents("../testdata/store.json", filepath.Join(testDir, "store.json"))
if err != nil {
t.Fatal(err)
}
_, signalLis := startSignal(t)
signalAddr = signalLis.Addr().String()
config.Signal.URI = signalAddr
_, mgmLis := startManagement(config, t)
mgmAddr = mgmLis.Addr().String()
}
func TestUp(t *testing.T) {
defer iface.Close()
tempDir := t.TempDir()
confPath := tempDir + "/config.json"
mgmtURL, err := url.Parse("http://" + mgmAddr)
if err != nil {
t.Fatal(err)
}
rootCmd.SetArgs([]string{
"up",
"--config",
confPath,
"--setup-key",
"A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
"--management-url",
mgmtURL.String(),
"--log-file",
"console",
})
go func() {
err = rootCmd.Execute()
if err != nil {
t.Errorf("expected no error while running up command, got %v", err)
}
}()
exists := false
for start := time.Now(); time.Since(start) < 15*time.Second; {
e, err := iface.Exists(iface.WgInterfaceDefault)
if err != nil {
continue
}
if *e {
exists = true
break
}
}
if !exists {
t.Errorf("expected wireguard interface %s to be created", iface.WgInterfaceDefault)
}
}

View File

@@ -1,18 +1,14 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/version"
)
import "github.com/spf13/cobra"
var (
Version string
versionCmd = &cobra.Command{
Use: "version",
Short: "prints Netbird version",
Short: "prints wiretrustee version",
Run: func(cmd *cobra.Command, args []string) {
cmd.SetOut(cmd.OutOrStdout())
cmd.Println(version.NetbirdVersion())
cmd.Println(Version)
},
}
)

View File

@@ -1,57 +0,0 @@
package firewall
import (
"net"
)
// Rule abstraction should be implemented by each firewall manager
//
// Each firewall type for different OS can use different type
// of the properties to hold data of the created rule
type Rule interface {
// GetRuleID returns the rule id
GetRuleID() string
}
// Direction is the direction of the traffic
type Direction int
const (
// DirectionSrc is the direction of the traffic from the source
DirectionSrc Direction = iota
// DirectionDst is the direction of the traffic from the destination
DirectionDst
)
// Action is the action to be taken on a rule
type Action int
const (
// ActionAccept is the action to accept a packet
ActionAccept Action = iota
// ActionDrop is the action to drop a packet
ActionDrop
)
// Manager is the high level abstraction of a firewall manager
//
// It declares methods which handle actions required by the
// Netbird client for ACL and routing functionality
type Manager interface {
// AddFiltering rule to the firewall
AddFiltering(
ip net.IP,
port *Port,
direction Direction,
action Action,
comment string,
) (Rule, error)
// DeleteRule from the firewall by rule definition
DeleteRule(rule Rule) error
// Reset firewall to the default state
Reset() error
// TODO: migrate routemanager firewal actions to this interface
}

View File

@@ -1,160 +0,0 @@
package iptables
import (
"fmt"
"net"
"strconv"
"sync"
"github.com/coreos/go-iptables/iptables"
"github.com/google/uuid"
fw "github.com/netbirdio/netbird/client/firewall"
)
const (
// ChainFilterName is the name of the chain that is used for filtering by the Netbird client
ChainFilterName = "NETBIRD-ACL"
)
// Manager of iptables firewall
type Manager struct {
mutex sync.Mutex
ipv4Client *iptables.IPTables
ipv6Client *iptables.IPTables
}
// Create iptables firewall manager
func Create() (*Manager, error) {
m := &Manager{}
// init clients for booth ipv4 and ipv6
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
if err != nil {
return nil, fmt.Errorf("iptables is not installed in the system or not supported")
}
m.ipv4Client = ipv4Client
ipv6Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
if err != nil {
return nil, fmt.Errorf("ip6tables is not installed in the system or not supported")
}
m.ipv6Client = ipv6Client
if err := m.Reset(); err != nil {
return nil, fmt.Errorf("failed to reset firewall: %s", err)
}
return m, nil
}
// AddFiltering rule to the firewall
func (m *Manager) AddFiltering(
ip net.IP,
port *fw.Port,
direction fw.Direction,
action fw.Action,
comment string,
) (fw.Rule, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
client := m.client(ip)
ok, err := client.ChainExists("filter", ChainFilterName)
if err != nil {
return nil, fmt.Errorf("failed to check if chain exists: %s", err)
}
if !ok {
if err := client.NewChain("filter", ChainFilterName); err != nil {
return nil, fmt.Errorf("failed to create chain: %s", err)
}
}
if port == nil || port.Values == nil || (port.IsRange && len(port.Values) != 2) {
return nil, fmt.Errorf("invalid port definition")
}
pv := strconv.Itoa(port.Values[0])
if port.IsRange {
pv += ":" + strconv.Itoa(port.Values[1])
}
specs := m.filterRuleSpecs("filter", ChainFilterName, ip, pv, direction, action, comment)
if err := client.AppendUnique("filter", ChainFilterName, specs...); err != nil {
return nil, err
}
rule := &Rule{
id: uuid.New().String(),
specs: specs,
v6: ip.To4() == nil,
}
return rule, nil
}
// DeleteRule from the firewall by rule definition
func (m *Manager) DeleteRule(rule fw.Rule) error {
m.mutex.Lock()
defer m.mutex.Unlock()
r, ok := rule.(*Rule)
if !ok {
return fmt.Errorf("invalid rule type")
}
client := m.ipv4Client
if r.v6 {
client = m.ipv6Client
}
return client.Delete("filter", ChainFilterName, r.specs...)
}
// Reset firewall to the default state
func (m *Manager) Reset() error {
m.mutex.Lock()
defer m.mutex.Unlock()
if err := m.reset(m.ipv4Client, "filter", ChainFilterName); err != nil {
return fmt.Errorf("clean ipv4 firewall ACL chain: %w", err)
}
if err := m.reset(m.ipv6Client, "filter", ChainFilterName); err != nil {
return fmt.Errorf("clean ipv6 firewall ACL chain: %w", err)
}
return nil
}
// reset firewall chain, clear it and drop it
func (m *Manager) reset(client *iptables.IPTables, table, chain string) error {
ok, err := client.ChainExists(table, chain)
if err != nil {
return fmt.Errorf("failed to check if chain exists: %w", err)
}
if !ok {
return nil
}
if err := client.ClearChain(table, ChainFilterName); err != nil {
return fmt.Errorf("failed to clear chain: %w", err)
}
return client.DeleteChain(table, ChainFilterName)
}
// filterRuleSpecs returns the specs of a filtering rule
func (m *Manager) filterRuleSpecs(
table string, chain string, ip net.IP, port string,
direction fw.Direction, action fw.Action, comment string,
) (specs []string) {
if direction == fw.DirectionSrc {
specs = append(specs, "-s", ip.String())
}
specs = append(specs, "-p", "tcp", "--dport", port)
specs = append(specs, "-j", m.actionToStr(action))
return append(specs, "-m", "comment", "--comment", comment)
}
// client returns corresponding iptables client for the given ip
func (m *Manager) client(ip net.IP) *iptables.IPTables {
if ip.To4() != nil {
return m.ipv4Client
}
return m.ipv6Client
}
func (m *Manager) actionToStr(action fw.Action) string {
if action == fw.ActionAccept {
return "ACCEPT"
}
return "DROP"
}

View File

@@ -1,105 +0,0 @@
package iptables
import (
"net"
"testing"
"github.com/coreos/go-iptables/iptables"
fw "github.com/netbirdio/netbird/client/firewall"
)
func TestNewManager(t *testing.T) {
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
if err != nil {
t.Fatal(err)
}
manager, err := Create()
if err != nil {
t.Fatal(err)
}
var rule1 fw.Rule
t.Run("add first rule", func(t *testing.T) {
ip := net.ParseIP("10.20.0.2")
port := &fw.Port{Proto: fw.PortProtocolTCP, Values: []int{8080}}
rule1, err = manager.AddFiltering(ip, port, fw.DirectionDst, fw.ActionAccept, "accept HTTP traffic")
if err != nil {
t.Errorf("failed to add rule: %v", err)
}
checkRuleSpecs(t, ipv4Client, true, rule1.(*Rule).specs...)
})
var rule2 fw.Rule
t.Run("add second rule", func(t *testing.T) {
ip := net.ParseIP("10.20.0.3")
port := &fw.Port{
Proto: fw.PortProtocolTCP,
Values: []int{8043: 8046},
}
rule2, err = manager.AddFiltering(
ip, port, fw.DirectionDst, fw.ActionAccept, "accept HTTPS traffic from ports range")
if err != nil {
t.Errorf("failed to add rule: %v", err)
}
checkRuleSpecs(t, ipv4Client, true, rule2.(*Rule).specs...)
})
t.Run("delete first rule", func(t *testing.T) {
if err := manager.DeleteRule(rule1); err != nil {
t.Errorf("failed to delete rule: %v", err)
}
checkRuleSpecs(t, ipv4Client, false, rule1.(*Rule).specs...)
})
t.Run("delete second rule", func(t *testing.T) {
if err := manager.DeleteRule(rule2); err != nil {
t.Errorf("failed to delete rule: %v", err)
}
checkRuleSpecs(t, ipv4Client, false, rule2.(*Rule).specs...)
})
t.Run("reset check", func(t *testing.T) {
// add second rule
ip := net.ParseIP("10.20.0.3")
port := &fw.Port{Proto: fw.PortProtocolUDP, Values: []int{5353}}
_, err = manager.AddFiltering(ip, port, fw.DirectionDst, fw.ActionAccept, "accept Fake DNS traffic")
if err != nil {
t.Errorf("failed to add rule: %v", err)
}
if err := manager.Reset(); err != nil {
t.Errorf("failed to reset: %v", err)
}
ok, err := ipv4Client.ChainExists("filter", ChainFilterName)
if err != nil {
t.Errorf("failed to drop chain: %v", err)
}
if ok {
t.Errorf("chain '%v' still exists after Reset", ChainFilterName)
}
})
}
func checkRuleSpecs(t *testing.T, ipv4Client *iptables.IPTables, mustExists bool, rulespec ...string) {
exists, err := ipv4Client.Exists("filter", ChainFilterName, rulespec...)
if err != nil {
t.Errorf("failed to check rule: %v", err)
return
}
if !exists && mustExists {
t.Errorf("rule '%v' does not exist", rulespec)
return
}
if exists && !mustExists {
t.Errorf("rule '%v' exist", rulespec)
return
}
}

View File

@@ -1,13 +0,0 @@
package iptables
// Rule to handle management of rules
type Rule struct {
id string
specs []string
v6 bool
}
// GetRuleID returns the rule id
func (r *Rule) GetRuleID() string {
return r.id
}

View File

@@ -1,24 +0,0 @@
package firewall
// PortProtocol is the protocol of the port
type PortProtocol string
const (
// PortProtocolTCP is the TCP protocol
PortProtocolTCP PortProtocol = "tcp"
// PortProtocolUDP is the UDP protocol
PortProtocolUDP PortProtocol = "udp"
)
// Port of the address for firewall rule
type Port struct {
// IsRange is true Values contains two values, the first is the start port, the second is the end port
IsRange bool
// Values contains one value for single port, multiple values for the list of ports, or two values for the range of ports
Values []int
// Proto is the protocol of the port
Proto PortProtocol
}

View File

@@ -1,12 +1,12 @@
!define APP_NAME "Netbird"
!define COMP_NAME "Netbird"
!define WEB_SITE "Netbird.io"
!define APP_NAME "Wiretrustee"
!define COMP_NAME "Wiretrustee"
!define WEB_SITE "wiretrustee.com"
!define VERSION $%APPVER%
!define COPYRIGHT "Netbird Authors, 2022"
!define COPYRIGHT "Wiretrustee Authors, 2021"
!define DESCRIPTION "A WireGuard®-based mesh network that connects your devices into a single private network"
!define INSTALLER_NAME "netbird-installer.exe"
!define MAIN_APP_EXE "Netbird"
!define ICON "ui\\netbird.ico"
!define INSTALLER_NAME "wiretrustee-installer.exe"
!define MAIN_APP_EXE "Wiretrustee"
!define ICON "ui\\wiretrustee.ico"
!define BANNER "ui\\banner.bmp"
!define LICENSE_DATA "..\\LICENSE"
@@ -15,13 +15,6 @@
!define REG_ROOT "HKLM"
!define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAIN_APP_EXE}"
!define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}"
!define UI_APP_NAME "Netbird UI"
!define UI_APP_EXE "Netbird-ui"
!define UI_REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${UI_APP_EXE}"
!define UI_UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UI_APP_NAME}"
Unicode True
######################################################################
@@ -51,13 +44,10 @@ ShowInstDetails Show
!define MUI_UNICON "${ICON}"
!define MUI_WELCOMEFINISHPAGE_BITMAP "${BANNER}"
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "${BANNER}"
!define MUI_FINISHPAGE_RUN
!define MUI_FINISHPAGE_RUN_TEXT "Start ${UI_APP_NAME}"
!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink"
######################################################################
!include "MUI2.nsh"
!include LogicLib.nsh
!define MUI_ABORTWARNING
!define MUI_UNABORTWARNING
@@ -82,135 +72,48 @@ ShowInstDetails Show
######################################################################
Function GetAppFromCommand
Exch $1
Push $2
StrCpy $2 $1 1 0
StrCmp $2 '"' 0 done
Push $3
StrCpy $3 ""
loop:
IntOp $3 $3 + 1
StrCpy $2 $1 1 $3
StrCmp $2 '' +2
StrCmp $2 '"' 0 loop
StrCpy $1 $1 $3
StrCpy $1 $1 "" 1 ; Remove starting quote
Pop $3
done:
Pop $2
Exch $1
FunctionEnd
!macro GetAppFromCommand in out
Push "${in}"
Call GetAppFromCommand
Pop ${out}
!macroend
!macro UninstallPreviousNSIS UninstCommand CustomParameters
Push $0
Push $1
Push $2
Push '${CustomParameters}'
Push '${UninstCommand}'
Call GetAppFromCommand ; Remove quotes and parameters from UninstCommand
Pop $0
Pop $1
GetFullPathName $2 "$0\.."
ExecWait '"$0" /S $1 _?=$2'
Delete "$0" ; Extra cleanup because we used _?=
RMDir "$2"
Pop $2
Pop $1
Pop $0
!macroend
Function .onInit
StrCpy $INSTDIR "${INSTALL_DIR}"
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
${If} $R0 != ""
# if silent install jump to uninstall step
IfSilent uninstall
MessageBox MB_YESNO|MB_ICONQUESTION "NetBird is already installed. We must remove it before installing upgrading NetBird. Proceed?" IDNO done IDYES uninstall
uninstall:
!insertmacro UninstallPreviousNSIS $R0 "/NoMsgBox"
done:
${EndIf}
FunctionEnd
######################################################################
Section -MainProgram
${INSTALL_TYPE}
# SetOverwrite ifnewer
SetOverwrite ifnewer
SetOutPath "$INSTDIR"
File /r "..\\dist\\netbird_windows_amd64\\"
File /r "..\\dist\\wiretrustee_windows_amd64\\"
SectionEnd
######################################################################
Section -Icons_Reg
SetOutPath "$INSTDIR"
WriteUninstaller "$INSTDIR\netbird_uninstall.exe"
WriteUninstaller "$INSTDIR\wiretrustee_uninstall.exe"
WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayName" "${APP_NAME}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "UninstallString" "$INSTDIR\netbird_uninstall.exe"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "UninstallString" "$INSTDIR\wiretrustee_uninstall.exe"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\${MAIN_APP_EXE}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayVersion" "${VERSION}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}"
WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
EnVar::SetHKLM
EnVar::AddValueEx "path" "$INSTDIR"
SetShellVarContext current
CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
SetShellVarContext all
SectionEnd
Section -Post
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service install'
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service start'
Exec '"$INSTDIR\${MAIN_APP_EXE}" service install'
# sleep a bit for visibility
Sleep 1000
SectionEnd
######################################################################
Section Uninstall
${INSTALL_TYPE}
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service stop'
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
# kill ui client
ExecWait `taskkill /im ${UI_APP_EXE}.exe`
Exec '"$INSTDIR\${MAIN_APP_EXE}" service stop'
Exec '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
# wait the service uninstall take unblock the executable
Sleep 3000
Delete "$INSTDIR\${UI_APP_EXE}"
Delete "$INSTDIR\${MAIN_APP_EXE}"
Delete "$INSTDIR\wintun.dll"
RmDir /r "$INSTDIR"
SetShellVarContext current
Delete "$DESKTOP\${APP_NAME}.lnk"
Delete "$SMPROGRAMS\${APP_NAME}.lnk"
SetShellVarContext all
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
EnVar::SetHKLM
EnVar::DeleteValue "path" "$INSTDIR"
SectionEnd
Function LaunchLink
SetShellVarContext current
SetOutPath $INSTDIR
ShellExecAsUser::ShellExecAsUser "" "$DESKTOP\${APP_NAME}.lnk"
SetShellVarContext all
FunctionEnd
SectionEnd

32
client/internal/cond.go Normal file
View File

@@ -0,0 +1,32 @@
package internal
import "sync"
// A Cond is a condition variable like sync.Cond, but using a channel so we can use select.
type Cond struct {
once sync.Once
C chan struct{}
}
// NewCond creates a new condition variable.
func NewCond() *Cond {
return &Cond{C: make(chan struct{})}
}
// Do runs f if the condition hasn't been signaled yet. Afterwards it will be signaled.
func (c *Cond) Do(f func()) {
c.once.Do(func() {
f()
close(c.C)
})
}
// Signal closes the condition variable channel.
func (c *Cond) Signal() {
c.Do(func() {})
}
// Wait waits for the condition variable channel to close.
func (c *Cond) Wait() {
<-c.C
}

View File

@@ -2,304 +2,117 @@ package internal
import (
"fmt"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/iface"
"github.com/wiretrustee/wiretrustee/util"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/util"
)
const (
// 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
ManagementLegacyPort = 33073
// DefaultManagementURL points to the NetBird's cloud management endpoint
DefaultManagementURL = "https://api.wiretrustee.com:443"
// DefaultAdminURL points to NetBird's cloud management console
DefaultAdminURL = "https://app.netbird.io:443"
)
var managementURLDefault *url.URL
var defaultInterfaceBlacklist = []string{iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts",
"Tailscale", "tailscale", "docker", "veth", "br-", "lo"}
func ManagementURLDefault() *url.URL {
return managementURLDefault
}
// ConfigInput carries configuration changes to the client
type ConfigInput struct {
ManagementURL string
AdminURL string
ConfigPath string
PreSharedKey *string
NATExternalIPs []string
CustomDNSAddress []byte
func init() {
managementURL, err := parseManagementURL("https://api.wiretrustee.com:33073")
if err != nil {
panic(err)
}
managementURLDefault = managementURL
}
// Config Configuration type
type Config struct {
// Wireguard private key of local peer
PrivateKey string
PreSharedKey string
ManagementURL *url.URL
AdminURL *url.URL
WgIface string
WgPort int
IFaceBlackList []string
DisableIPv6Discovery bool
// SSHKey is a private SSH key in a PEM format
SSHKey string
// ExternalIP mappings, if different than the host interface IP
//
// External IP must not be behind a CGNAT and port-forwarding for incoming UDP packets from WgPort on ExternalIP
// to WgPort on host interface IP must be present. This can take form of single port-forwarding rule, 1:1 DNAT
// mapping ExternalIP to host interface IP, or a NAT DMZ to host interface IP.
//
// A single mapping will take the form of: external[/internal]
// external (required): either the external IP address or "stun" to use STUN to determine the external IP address
// internal (optional): either the internal/interface IP address or an interface name
//
// examples:
// "12.34.56.78" => all interfaces IPs will be mapped to external IP of 12.34.56.78
// "12.34.56.78/eth0" => IPv4 assigned to interface eth0 will be mapped to external IP of 12.34.56.78
// "12.34.56.78/10.1.2.3" => interface IP 10.1.2.3 will be mapped to external IP of 12.34.56.78
NATExternalIPs []string
// CustomDNSAddress sets the DNS resolver listening address in format ip:port
CustomDNSAddress string
PrivateKey string
PreSharedKey string
ManagementURL *url.URL
WgIface string
IFaceBlackList []string
}
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
func ReadConfig(configPath string) (*Config, error) {
if configFileIsExists(configPath) {
config := &Config{}
if _, err := util.ReadJson(configPath, config); err != nil {
return nil, err
}
return config, nil
}
cfg, err := createNewConfig(ConfigInput{ConfigPath: configPath})
if err != nil {
return nil, err
}
err = WriteOutConfig(configPath, cfg)
return cfg, err
}
// UpdateConfig update existing configuration according to input configuration and return with the configuration
func UpdateConfig(input ConfigInput) (*Config, error) {
if !configFileIsExists(input.ConfigPath) {
return nil, status.Errorf(codes.NotFound, "config file doesn't exist")
}
return update(input)
}
// UpdateOrCreateConfig reads existing config or generates a new one
func UpdateOrCreateConfig(input ConfigInput) (*Config, error) {
if !configFileIsExists(input.ConfigPath) {
log.Infof("generating new config %s", input.ConfigPath)
cfg, err := createNewConfig(input)
//createNewConfig creates a new config generating a new Wireguard key and saving to file
func createNewConfig(managementURL string, configPath string, preSharedKey string) (*Config, error) {
wgKey := generateKey()
config := &Config{PrivateKey: wgKey, WgIface: iface.WgInterfaceDefault, IFaceBlackList: []string{}}
if managementURL != "" {
URL, err := parseManagementURL(managementURL)
if err != nil {
return nil, err
}
err = WriteOutConfig(input.ConfigPath, cfg)
return cfg, err
config.ManagementURL = URL
} else {
config.ManagementURL = managementURLDefault
}
if isPreSharedKeyHidden(input.PreSharedKey) {
input.PreSharedKey = nil
}
return update(input)
}
// CreateInMemoryConfig generate a new config but do not write out it to the store
func CreateInMemoryConfig(input ConfigInput) (*Config, error) {
return createNewConfig(input)
}
// WriteOutConfig write put the prepared config to the given path
func WriteOutConfig(path string, config *Config) error {
return util.WriteJson(path, config)
}
// createNewConfig creates a new config generating a new Wireguard key and saving to file
func createNewConfig(input ConfigInput) (*Config, error) {
wgKey := generateKey()
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
if err != nil {
return nil, err
}
config := &Config{
SSHKey: string(pem),
PrivateKey: wgKey,
WgIface: iface.WgInterfaceDefault,
WgPort: iface.DefaultWgPort,
IFaceBlackList: []string{},
DisableIPv6Discovery: false,
NATExternalIPs: input.NATExternalIPs,
CustomDNSAddress: string(input.CustomDNSAddress),
if preSharedKey != "" {
config.PreSharedKey = preSharedKey
}
defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
config.IFaceBlackList = []string{iface.WgInterfaceDefault, "tun0"}
err := util.WriteJson(configPath, config)
if err != nil {
return nil, err
}
config.ManagementURL = defaultManagementURL
if input.ManagementURL != "" {
URL, err := parseURL("Management URL", input.ManagementURL)
return config, nil
}
func parseManagementURL(managementURL string) (*url.URL, error) {
parsedMgmtURL, err := url.ParseRequestURI(managementURL)
if err != nil {
log.Errorf("failed parsing management URL %s: [%s]", managementURL, err.Error())
return nil, err
}
if !(parsedMgmtURL.Scheme == "https" || parsedMgmtURL.Scheme == "http") {
return nil, fmt.Errorf("invalid Management Service URL provided %s. Supported format [http|https]://[host]:[port]", managementURL)
}
return parsedMgmtURL, err
}
// ReadConfig reads existing config. In case provided managementURL is not empty overrides the read property
func ReadConfig(managementURL string, configPath string) (*Config, error) {
config := &Config{}
_, err := util.ReadJson(configPath, config)
if err != nil {
return nil, err
}
if managementURL != "" {
URL, err := parseManagementURL(managementURL)
if err != nil {
return nil, err
}
config.ManagementURL = URL
}
if input.PreSharedKey != nil {
config.PreSharedKey = *input.PreSharedKey
}
defaultAdminURL, err := parseURL("Admin URL", DefaultAdminURL)
if err != nil {
return nil, err
}
config.AdminURL = defaultAdminURL
if input.AdminURL != "" {
newURL, err := parseURL("Admin Panel URL", input.AdminURL)
if err != nil {
return nil, err
}
config.AdminURL = newURL
}
config.IFaceBlackList = defaultInterfaceBlacklist
return config, nil
return config, err
}
func update(input ConfigInput) (*Config, error) {
config := &Config{}
// GetConfig reads existing config or generates a new one
func GetConfig(managementURL string, configPath string, preSharedKey string) (*Config, error) {
if _, err := util.ReadJson(input.ConfigPath, config); err != nil {
return nil, err
if _, err := os.Stat(configPath); os.IsNotExist(err) {
log.Infof("generating new config %s", configPath)
return createNewConfig(managementURL, configPath, preSharedKey)
} else {
return ReadConfig(managementURL, configPath)
}
refresh := false
if input.ManagementURL != "" && config.ManagementURL.String() != input.ManagementURL {
log.Infof("new Management URL provided, updated to %s (old value %s)",
input.ManagementURL, config.ManagementURL)
newURL, err := parseURL("Management URL", input.ManagementURL)
if err != nil {
return nil, err
}
config.ManagementURL = newURL
refresh = true
}
if input.AdminURL != "" && (config.AdminURL == nil || config.AdminURL.String() != input.AdminURL) {
log.Infof("new Admin Panel URL provided, updated to %s (old value %s)",
input.AdminURL, config.AdminURL)
newURL, err := parseURL("Admin Panel URL", input.AdminURL)
if err != nil {
return nil, err
}
config.AdminURL = newURL
refresh = true
}
if input.PreSharedKey != nil && config.PreSharedKey != *input.PreSharedKey {
log.Infof("new pre-shared key provided, updated to %s (old value %s)",
*input.PreSharedKey, config.PreSharedKey)
config.PreSharedKey = *input.PreSharedKey
refresh = true
}
if config.SSHKey == "" {
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
if err != nil {
return nil, err
}
config.SSHKey = string(pem)
refresh = true
}
if config.WgPort == 0 {
config.WgPort = iface.DefaultWgPort
refresh = true
}
if input.NATExternalIPs != nil && len(config.NATExternalIPs) != len(input.NATExternalIPs) {
config.NATExternalIPs = input.NATExternalIPs
refresh = true
}
if input.CustomDNSAddress != nil {
config.CustomDNSAddress = string(input.CustomDNSAddress)
refresh = true
}
if refresh {
// since we have new management URL, we need to update config file
if err := util.WriteJson(input.ConfigPath, config); err != nil {
return nil, err
}
}
return config, nil
}
// parseURL parses and validates a service URL
func parseURL(serviceName, serviceURL string) (*url.URL, error) {
parsedMgmtURL, err := url.ParseRequestURI(serviceURL)
if err != nil {
log.Errorf("failed parsing %s URL %s: [%s]", serviceName, serviceURL, err.Error())
return nil, err
}
if parsedMgmtURL.Scheme != "https" && parsedMgmtURL.Scheme != "http" {
return nil, fmt.Errorf(
"invalid %s URL provided %s. Supported format [http|https]://[host]:[port]",
serviceName, serviceURL)
}
if parsedMgmtURL.Port() == "" {
switch parsedMgmtURL.Scheme {
case "https":
parsedMgmtURL.Host = parsedMgmtURL.Host + ":443"
case "http":
parsedMgmtURL.Host = parsedMgmtURL.Host + ":80"
default:
log.Infof("unable to determine a default port for schema %s in URL %s", parsedMgmtURL.Scheme, serviceURL)
}
}
return parsedMgmtURL, err
}
// generateKey generates a new Wireguard private key
func generateKey() string {
key, err := wgtypes.GeneratePrivateKey()
key, err := wgtypes.GenerateKey()
if err != nil {
panic(err)
}
return key.String()
}
// don't overwrite pre-shared key if we receive asterisks from UI
func isPreSharedKeyHidden(preSharedKey *string) bool {
if preSharedKey != nil && *preSharedKey == "**********" {
return true
}
return false
}
func configFileIsExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}

View File

@@ -1,124 +0,0 @@
package internal
import (
"errors"
"os"
"path/filepath"
"testing"
"github.com/netbirdio/netbird/util"
"github.com/stretchr/testify/assert"
)
func TestGetConfig(t *testing.T) {
// case 1: new default config has to be generated
config, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
})
if err != nil {
return
}
assert.Equal(t, config.ManagementURL.String(), DefaultManagementURL)
assert.Equal(t, config.AdminURL.String(), DefaultAdminURL)
if err != nil {
return
}
managementURL := "https://test.management.url:33071"
adminURL := "https://app.admin.url:443"
path := filepath.Join(t.TempDir(), "config.json")
preSharedKey := "preSharedKey"
// case 2: new config has to be generated
config, err = UpdateOrCreateConfig(ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: path,
PreSharedKey: &preSharedKey,
})
if err != nil {
return
}
assert.Equal(t, config.ManagementURL.String(), managementURL)
assert.Equal(t, config.PreSharedKey, preSharedKey)
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
t.Errorf("config file was expected to be created under path %s", path)
}
// case 3: existing config -> fetch it
config, err = UpdateOrCreateConfig(ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: path,
PreSharedKey: &preSharedKey,
})
if err != nil {
return
}
assert.Equal(t, config.ManagementURL.String(), managementURL)
assert.Equal(t, config.PreSharedKey, preSharedKey)
// case 4: existing config, but new managementURL has been provided -> update config
newManagementURL := "https://test.newManagement.url:33071"
config, err = UpdateOrCreateConfig(ConfigInput{
ManagementURL: newManagementURL,
AdminURL: adminURL,
ConfigPath: path,
PreSharedKey: &preSharedKey,
})
if err != nil {
return
}
assert.Equal(t, config.ManagementURL.String(), newManagementURL)
assert.Equal(t, config.PreSharedKey, preSharedKey)
// read once more to make sure that config file has been updated with the new management URL
readConf, err := util.ReadJson(path, config)
if err != nil {
return
}
assert.Equal(t, readConf.(*Config).ManagementURL.String(), newManagementURL)
}
func TestHiddenPreSharedKey(t *testing.T) {
hidden := "**********"
samplePreSharedKey := "mysecretpresharedkey"
tests := []struct {
name string
preSharedKey *string
want string
}{
{"nil", nil, ""},
{"hidden", &hidden, ""},
{"filled", &samplePreSharedKey, samplePreSharedKey},
}
// generate default cfg
cfgFile := filepath.Join(t.TempDir(), "config.json")
_, _ = UpdateOrCreateConfig(ConfigInput{
ConfigPath: cfgFile,
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: cfgFile,
PreSharedKey: tt.preSharedKey,
})
if err != nil {
t.Fatalf("failed to get cfg: %s", err)
}
if cfg.PreSharedKey != tt.want {
t.Fatalf("invalid preshared key: '%s', expected: '%s' ", cfg.PreSharedKey, tt.want)
}
})
}
}

View File

@@ -1,349 +0,0 @@
package internal
import (
"context"
"fmt"
"strings"
"time"
"github.com/cenkalti/backoff/v4"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/iface"
mgm "github.com/netbirdio/netbird/management/client"
mgmProto "github.com/netbirdio/netbird/management/proto"
signal "github.com/netbirdio/netbird/signal/client"
)
// RunClient with main logic.
func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover) error {
backOff := &backoff.ExponentialBackOff{
InitialInterval: time.Second,
RandomizationFactor: 1,
Multiplier: 1.7,
MaxInterval: 15 * time.Second,
MaxElapsedTime: 3 * 30 * 24 * time.Hour, // 3 months
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}
state := CtxGetState(ctx)
defer func() {
s, err := state.Status()
if err != nil || s != StatusNeedsLogin {
state.Set(StatusIdle)
}
}()
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
}
defer statusRecorder.ClientStop()
operation := func() error {
// if context cancelled we not start new backoff cycle
select {
case <-ctx.Done():
return nil
default:
}
state.Set(StatusConnecting)
engineCtx, cancel := context.WithCancel(ctx)
defer func() {
statusRecorder.MarkManagementDisconnected()
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))
}
mgmNotifier := statusRecorderToMgmConnStateNotifier(statusRecorder)
mgmClient.SetConnStateListener(mgmNotifier)
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
loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey)
if err != nil {
log.Debug(err)
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
state.Set(StatusNeedsLogin)
return backoff.Permanent(wrapErr(err)) // unrecoverable error
}
return wrapErr(err)
}
statusRecorder.MarkManagementConnected()
localPeerState := peer.LocalPeerState{
IP: loginResp.GetPeerConfig().GetAddress(),
PubKey: myPrivateKey.PublicKey().String(),
KernelInterface: iface.WireGuardModuleIsLoaded(),
FQDN: loginResp.GetPeerConfig().GetFqdn(),
}
statusRecorder.UpdateLocalPeerState(localPeerState)
signalURL := fmt.Sprintf("%s://%s",
strings.ToLower(loginResp.GetWiretrusteeConfig().GetSignal().GetProtocol().String()),
loginResp.GetWiretrusteeConfig().GetSignal().GetUri(),
)
statusRecorder.UpdateSignalAddress(signalURL)
statusRecorder.MarkSignalDisconnected()
defer statusRecorder.MarkSignalDisconnected()
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
if err != nil {
log.Error(err)
return wrapErr(err)
}
defer func() {
err = signalClient.Close()
if err != nil {
log.Warnf("failed closing Signal service client %v", err)
}
}()
signalNotifier := statusRecorderToSignalConnStateNotifier(statusRecorder)
signalClient.SetConnStateListener(signalNotifier)
statusRecorder.MarkSignalConnected()
peerConfig := loginResp.GetPeerConfig()
engineConfig, err := createEngineConfig(myPrivateKey, config, peerConfig)
if err != nil {
log.Error(err)
return wrapErr(err)
}
md, err := newMobileDependency(tunAdapter, iFaceDiscover, mgmClient)
if err != nil {
log.Error(err)
return wrapErr(err)
}
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig, md, statusRecorder)
err = engine.Start()
if err != nil {
log.Errorf("error while starting Netbird Connection Engine: %s", err)
return wrapErr(err)
}
log.Print("Netbird engine started, my IP is: ", peerConfig.Address)
state.Set(StatusConnected)
statusRecorder.ClientStart()
<-engineCtx.Done()
statusRecorder.ClientTeardown()
backOff.Reset()
err = engine.Stop()
if err != nil {
log.Errorf("failed stopping engine %v", err)
return wrapErr(err)
}
log.Info("stopped NetBird client")
if _, err := state.Status(); err == ErrResetConnection {
return err
}
return nil
}
err = backoff.Retry(operation, backOff)
if err != nil {
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
state.Set(StatusNeedsLogin)
}
return err
}
return nil
}
// createEngineConfig converts configuration received from Management Service to EngineConfig
func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) {
engineConf := &EngineConfig{
WgIfaceName: config.WgIface,
WgAddr: peerConfig.Address,
IFaceBlackList: config.IFaceBlackList,
DisableIPv6Discovery: config.DisableIPv6Discovery,
WgPrivateKey: key,
WgPort: config.WgPort,
SSHKey: []byte(config.SSHKey),
NATExternalIPs: config.NATExternalIPs,
CustomDNSAddress: config.CustomDNSAddress,
}
if config.PreSharedKey != "" {
preSharedKey, err := wgtypes.ParseKey(config.PreSharedKey)
if err != nil {
return nil, err
}
engineConf.PreSharedKey = &preSharedKey
}
return engineConf, nil
}
// connectToSignal creates Signal Service client and established a connection
func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig, ourPrivateKey wgtypes.Key) (*signal.GrpcClient, error) {
var sigTLSEnabled bool
if wtConfig.Signal.Protocol == mgmProto.HostConfig_HTTPS {
sigTLSEnabled = true
} else {
sigTLSEnabled = false
}
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, gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Signal Service : %s", err)
}
return signalClient, nil
}
// 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, gstatus.Errorf(codes.FailedPrecondition, "failed while getting Management Service public key: %s", err)
}
sysInfo := system.GetInfo(ctx)
loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey)
if err != nil {
return nil, err
}
return loginResp, nil
}
// 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) {
defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
if err != nil {
return nil, err
}
if config.ManagementURL.Hostname() != defaultManagementURL.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 := UpdateConfig(ConfigInput{
ManagementURL: newURL.String(),
ConfigPath: configPath,
})
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
}
func statusRecorderToMgmConnStateNotifier(statusRecorder *peer.Status) mgm.ConnStateNotifier {
var sri interface{} = statusRecorder
mgmNotifier, _ := sri.(mgm.ConnStateNotifier)
return mgmNotifier
}
func statusRecorderToSignalConnStateNotifier(statusRecorder *peer.Status) signal.ConnStateNotifier {
var sri interface{} = statusRecorder
notifier, _ := sri.(signal.ConnStateNotifier)
return notifier
}

View File

@@ -0,0 +1,425 @@
package internal
import (
"context"
"fmt"
ice "github.com/pion/ice/v2"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/iface"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"net"
"sync"
"time"
)
var (
// DefaultWgKeepAlive default Wireguard keep alive constant
DefaultWgKeepAlive = 20 * time.Second
privateIPBlocks []*net.IPNet
)
type Status string
const (
StatusConnected Status = "Connected"
StatusConnecting Status = "Connecting"
StatusDisconnected Status = "Disconnected"
)
func init() {
for _, cidr := range []string{
"127.0.0.0/8", // IPv4 loopback
"10.0.0.0/8", // RFC1918
"172.16.0.0/12", // RFC1918
"192.168.0.0/16", // RFC1918
"169.254.0.0/16", // RFC3927 link-local
"::1/128", // IPv6 loopback
"fe80::/10", // IPv6 link-local
"fc00::/7", // IPv6 unique local addr
} {
_, block, err := net.ParseCIDR(cidr)
if err != nil {
panic(fmt.Errorf("parse error on %q: %v", cidr, err))
}
privateIPBlocks = append(privateIPBlocks, block)
}
}
// ConnConfig Connection configuration struct
type ConnConfig struct {
// Local Wireguard listening address e.g. 127.0.0.1:51820
WgListenAddr string
// A Local Wireguard Peer IP address in CIDR notation e.g. 10.30.30.1/24
WgPeerIP string
// Local Wireguard Interface name (e.g. wg0)
WgIface string
// Wireguard allowed IPs (e.g. 10.30.30.2/32)
WgAllowedIPs string
// Local Wireguard private key
WgKey wgtypes.Key
// Remote Wireguard public key
RemoteWgKey wgtypes.Key
PreSharedKey *wgtypes.Key
StunTurnURLS []*ice.URL
iFaceBlackList map[string]struct{}
}
// IceCredentials ICE protocol credentials struct
type IceCredentials struct {
uFrag string
pwd string
}
// Connection Holds information about a connection and handles signal protocol
type Connection struct {
Config ConnConfig
// signalCandidate is a handler function to signal remote peer about local connection candidate
signalCandidate func(candidate ice.Candidate) error
// signalOffer is a handler function to signal remote peer our connection offer (credentials)
signalOffer func(uFrag string, pwd string) error
// signalOffer is a handler function to signal remote peer our connection answer (credentials)
signalAnswer func(uFrag string, pwd string) error
// remoteAuthChannel is a channel used to wait for remote credentials to proceed with the connection
remoteAuthChannel chan IceCredentials
// agent is an actual ice.Agent that is used to negotiate and maintain a connection to a remote peer
agent *ice.Agent
wgProxy *WgProxy
connected *Cond
closeCond *Cond
remoteAuthCond sync.Once
Status Status
}
// NewConnection Creates a new connection and sets handling functions for signal protocol
func NewConnection(config ConnConfig,
signalCandidate func(candidate ice.Candidate) error,
signalOffer func(uFrag string, pwd string) error,
signalAnswer func(uFrag string, pwd string) error,
) *Connection {
return &Connection{
Config: config,
signalCandidate: signalCandidate,
signalOffer: signalOffer,
signalAnswer: signalAnswer,
remoteAuthChannel: make(chan IceCredentials, 1),
closeCond: NewCond(),
connected: NewCond(),
agent: nil,
wgProxy: NewWgProxy(config.WgIface, config.RemoteWgKey.String(), config.WgAllowedIPs, config.WgListenAddr, config.PreSharedKey),
Status: StatusDisconnected,
}
}
// Open opens connection to a remote peer.
// Will block until the connection has successfully established
func (conn *Connection) Open(timeout time.Duration) error {
// create an ice.Agent that will be responsible for negotiating and establishing actual peer-to-peer connection
a, err := ice.NewAgent(&ice.AgentConfig{
// MulticastDNSMode: ice.MulticastDNSModeQueryAndGather,
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4},
Urls: conn.Config.StunTurnURLS,
CandidateTypes: []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive, ice.CandidateTypeRelay},
InterfaceFilter: func(s string) bool {
if conn.Config.iFaceBlackList == nil {
return true
}
_, ok := conn.Config.iFaceBlackList[s]
return !ok
},
})
if err != nil {
return err
}
conn.agent = a
defer func() {
err := conn.agent.Close()
if err != nil {
return
}
}()
err = conn.listenOnLocalCandidates()
if err != nil {
return err
}
err = conn.listenOnConnectionStateChanges()
if err != nil {
return err
}
err = conn.signalCredentials()
if err != nil {
return err
}
conn.Status = StatusConnecting
log.Debugf("trying to connect to peer %s", conn.Config.RemoteWgKey.String())
// wait until credentials have been sent from the remote peer (will arrive via a signal server)
select {
case remoteAuth := <-conn.remoteAuthChannel:
log.Debugf("got a connection confirmation from peer %s", conn.Config.RemoteWgKey.String())
err = conn.agent.GatherCandidates()
if err != nil {
return err
}
isControlling := conn.Config.WgKey.PublicKey().String() > conn.Config.RemoteWgKey.String()
var remoteConn *ice.Conn
remoteConn, err = conn.openConnectionToRemote(isControlling, remoteAuth)
if err != nil {
log.Errorf("failed establishing connection with the remote peer %s %s", conn.Config.RemoteWgKey.String(), err)
return err
}
var pair *ice.CandidatePair
pair, err = conn.agent.GetSelectedCandidatePair()
if err != nil {
return err
}
useProxy := useProxy(pair)
// in case the remote peer is in the local network or one of the peers has public static IP -> no need for a Wireguard proxy, direct communication is possible.
if !useProxy {
log.Debugf("it is possible to establish a direct connection (without proxy) to peer %s - my addr: %s, remote addr: %s", conn.Config.RemoteWgKey.String(), pair.Local, pair.Remote)
err = conn.wgProxy.StartLocal(fmt.Sprintf("%s:%d", pair.Remote.Address(), iface.WgPort))
if err != nil {
return err
}
} else {
log.Debugf("establishing secure tunnel to peer %s via selected candidate pair %s", conn.Config.RemoteWgKey.String(), pair)
err = conn.wgProxy.Start(remoteConn)
if err != nil {
return err
}
}
relayed := pair.Remote.Type() == ice.CandidateTypeRelay || pair.Local.Type() == ice.CandidateTypeRelay
conn.Status = StatusConnected
log.Infof("opened connection to peer %s [localProxy=%v, relayed=%v]", conn.Config.RemoteWgKey.String(), useProxy, relayed)
case <-conn.closeCond.C:
conn.Status = StatusDisconnected
return fmt.Errorf("connection to peer %s has been closed", conn.Config.RemoteWgKey.String())
case <-time.After(timeout):
err = conn.Close()
if err != nil {
log.Warnf("error while closing connection to peer %s -> %s", conn.Config.RemoteWgKey.String(), err.Error())
}
conn.Status = StatusDisconnected
return fmt.Errorf("timeout of %vs exceeded while waiting for the remote peer %s", timeout.Seconds(), conn.Config.RemoteWgKey.String())
}
// wait until connection has been closed
<-conn.closeCond.C
conn.Status = StatusDisconnected
return fmt.Errorf("connection to peer %s has been closed", conn.Config.RemoteWgKey.String())
}
func isPublicIP(ip net.IP) bool {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return false
}
for _, block := range privateIPBlocks {
if block.Contains(ip) {
return false
}
}
return true
}
//useProxy determines whether a direct connection (without a go proxy) is possible
//There are 3 cases: one of the peers has a public IP or both peers are in the same private network
//Please note, that this check happens when peers were already able to ping each other with ICE layer.
func useProxy(pair *ice.CandidatePair) bool {
remoteIP := net.ParseIP(pair.Remote.Address())
myIp := net.ParseIP(pair.Local.Address())
remoteIsPublic := isPublicIP(remoteIP)
myIsPublic := isPublicIP(myIp)
//one of the hosts has a public IP
if remoteIsPublic && pair.Remote.Type() == ice.CandidateTypeHost {
return false
}
if myIsPublic && pair.Local.Type() == ice.CandidateTypeHost {
return false
}
if pair.Local.Type() == ice.CandidateTypeHost && pair.Remote.Type() == ice.CandidateTypeHost {
if !remoteIsPublic && !myIsPublic {
//both hosts are in the same private network
return false
}
}
return true
}
// Close Closes a peer connection
func (conn *Connection) Close() error {
var err error
conn.closeCond.Do(func() {
log.Debugf("closing connection to peer %s", conn.Config.RemoteWgKey.String())
if a := conn.agent; a != nil {
e := a.Close()
if e != nil {
log.Warnf("error while closing ICE agent of peer connection %s", conn.Config.RemoteWgKey.String())
err = e
}
}
if c := conn.wgProxy; c != nil {
e := c.Close()
if e != nil {
log.Warnf("error while closingWireguard proxy connection of peer connection %s", conn.Config.RemoteWgKey.String())
err = e
}
}
})
return err
}
// OnAnswer Handles the answer from the other peer
func (conn *Connection) OnAnswer(remoteAuth IceCredentials) error {
conn.remoteAuthCond.Do(func() {
log.Debugf("OnAnswer from peer %s", conn.Config.RemoteWgKey.String())
conn.remoteAuthChannel <- remoteAuth
})
return nil
}
// OnOffer Handles the offer from the other peer
func (conn *Connection) OnOffer(remoteAuth IceCredentials) error {
conn.remoteAuthCond.Do(func() {
log.Debugf("OnOffer from peer %s", conn.Config.RemoteWgKey.String())
conn.remoteAuthChannel <- remoteAuth
uFrag, pwd, err := conn.agent.GetLocalUserCredentials()
if err != nil { //nolint
}
err = conn.signalAnswer(uFrag, pwd)
if err != nil { //nolint
}
})
return nil
}
// OnRemoteCandidate Handles remote candidate provided by the peer.
func (conn *Connection) OnRemoteCandidate(candidate ice.Candidate) error {
log.Debugf("onRemoteCandidate from peer %s -> %s", conn.Config.RemoteWgKey.String(), candidate.String())
err := conn.agent.AddRemoteCandidate(candidate)
if err != nil {
return err
}
return nil
}
// openConnectionToRemote opens an ice.Conn to the remote peer. This is a real peer-to-peer connection
// blocks until connection has been established
func (conn *Connection) openConnectionToRemote(isControlling bool, credentials IceCredentials) (*ice.Conn, error) {
var realConn *ice.Conn
var err error
if isControlling {
realConn, err = conn.agent.Dial(context.TODO(), credentials.uFrag, credentials.pwd)
} else {
realConn, err = conn.agent.Accept(context.TODO(), credentials.uFrag, credentials.pwd)
}
if err != nil {
return nil, err
}
return realConn, err
}
// signalCredentials prepares local user credentials and signals them to the remote peer
func (conn *Connection) signalCredentials() error {
localUFrag, localPwd, err := conn.agent.GetLocalUserCredentials()
if err != nil {
return err
}
err = conn.signalOffer(localUFrag, localPwd)
if err != nil {
return err
}
return nil
}
// listenOnLocalCandidates registers callback of an ICE Agent to receive new local connection candidates and then
// signals them to the remote peer
func (conn *Connection) listenOnLocalCandidates() error {
err := conn.agent.OnCandidate(func(candidate ice.Candidate) {
if candidate != nil {
log.Debugf("discovered local candidate %s", candidate.String())
err := conn.signalCandidate(candidate)
if err != nil {
log.Errorf("failed signaling candidate to the remote peer %s %s", conn.Config.RemoteWgKey.String(), err)
//todo ??
return
}
}
})
if err != nil {
return err
}
return nil
}
// listenOnConnectionStateChanges registers callback of an ICE Agent to track connection state
func (conn *Connection) listenOnConnectionStateChanges() error {
err := conn.agent.OnConnectionStateChange(func(state ice.ConnectionState) {
log.Debugf("ICE Connection State has changed for peer %s -> %s", conn.Config.RemoteWgKey.String(), state.String())
if state == ice.ConnectionStateConnected {
// closed the connection has been established we can check the selected candidate pair
pair, err := conn.agent.GetSelectedCandidatePair()
if err != nil {
log.Errorf("failed selecting active ICE candidate pair %s", err)
return
}
log.Debugf("ICE connected to peer %s via a selected connnection candidate pair %s", conn.Config.RemoteWgKey.String(), pair)
} else if state == ice.ConnectionStateDisconnected || state == ice.ConnectionStateFailed {
err := conn.Close()
if err != nil {
log.Warnf("error while closing connection to peer %s -> %s", conn.Config.RemoteWgKey.String(), err.Error())
}
}
})
if err != nil {
return err
}
return nil
}

View File

@@ -1,134 +0,0 @@
package internal
import (
"context"
"fmt"
"net/url"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
mgm "github.com/netbirdio/netbird/management/client"
)
// DeviceAuthorizationFlow represents Device Authorization Flow information
type DeviceAuthorizationFlow struct {
Provider string
ProviderConfig ProviderConfig
}
// ProviderConfig has all attributes needed to initiate a device authorization flow
type ProviderConfig struct {
// ClientID An IDP application client id
ClientID string
// 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
// Scopes provides the scopes to be included in the token request
Scope string
// UseIDToken indicates if the id token should be used for authentication
UseIDToken bool
}
// GetDeviceAuthorizationFlowInfo initialize a DeviceAuthorizationFlow instance and return with it
func GetDeviceAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmURL *url.URL) (DeviceAuthorizationFlow, error) {
// validate our peer's Wireguard PRIVATE key
myPrivateKey, err := wgtypes.ParseKey(privateKey)
if err != nil {
log.Errorf("failed parsing Wireguard key %s: [%s]", privateKey, err.Error())
return DeviceAuthorizationFlow{}, err
}
var mgmTLSEnabled bool
if mgmURL.Scheme == "https" {
mgmTLSEnabled = true
}
log.Debugf("connecting to Management Service %s", mgmURL.String())
mgmClient, err := mgm.NewClient(ctx, mgmURL.Host, myPrivateKey, mgmTLSEnabled)
if err != nil {
log.Errorf("failed connecting to Management Service %s %v", mgmURL.String(), err)
return DeviceAuthorizationFlow{}, err
}
log.Debugf("connected to the Management service %s", mgmURL.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 {
log.Errorf("failed while getting Management Service public key: %v", err)
return DeviceAuthorizationFlow{}, err
}
protoDeviceAuthorizationFlow, err := mgmClient.GetDeviceAuthorizationFlow(*serverKey)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
log.Warnf("server couldn't find device flow, contact admin: %v", err)
return DeviceAuthorizationFlow{}, err
}
log.Errorf("failed to retrieve device flow: %v", err)
return DeviceAuthorizationFlow{}, err
}
deviceAuthorizationFlow := DeviceAuthorizationFlow{
Provider: protoDeviceAuthorizationFlow.Provider.String(),
ProviderConfig: ProviderConfig{
Audience: protoDeviceAuthorizationFlow.GetProviderConfig().GetAudience(),
ClientID: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientID(),
ClientSecret: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientSecret(),
Domain: protoDeviceAuthorizationFlow.GetProviderConfig().Domain,
TokenEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetTokenEndpoint(),
DeviceAuthEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetDeviceAuthEndpoint(),
Scope: protoDeviceAuthorizationFlow.GetProviderConfig().GetScope(),
UseIDToken: protoDeviceAuthorizationFlow.GetProviderConfig().GetUseIDToken(),
},
}
// keep compatibility with older management versions
if deviceAuthorizationFlow.ProviderConfig.Scope == "" {
deviceAuthorizationFlow.ProviderConfig.Scope = "openid"
}
err = isProviderConfigValid(deviceAuthorizationFlow.ProviderConfig)
if err != nil {
return DeviceAuthorizationFlow{}, err
}
return deviceAuthorizationFlow, nil
}
func isProviderConfigValid(config ProviderConfig) error {
errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator"
if config.Audience == "" {
return fmt.Errorf(errorMSGFormat, "Audience")
}
if config.ClientID == "" {
return fmt.Errorf(errorMSGFormat, "Client ID")
}
if config.TokenEndpoint == "" {
return fmt.Errorf(errorMSGFormat, "Token Endpoint")
}
if config.DeviceAuthEndpoint == "" {
return fmt.Errorf(errorMSGFormat, "Device Auth Endpoint")
}
if config.Scope == "" {
return fmt.Errorf(errorMSGFormat, "Device Auth Scopes")
}
return nil
}

View File

@@ -1,41 +0,0 @@
package dns
import (
"context"
"github.com/godbus/dbus/v5"
log "github.com/sirupsen/logrus"
"time"
)
const dbusDefaultFlag = 0
func isDbusListenerRunning(dest string, path dbus.ObjectPath) bool {
obj, closeConn, err := getDbusObject(dest, path)
if err != nil {
return false
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Store()
return err == nil
}
func getDbusObject(dest string, path dbus.ObjectPath) (dbus.BusObject, func(), error) {
conn, err := dbus.SystemBus()
if err != nil {
return nil, nil, err
}
obj := conn.Object(dest, path)
closeFunc := func() {
closeErr := conn.Close()
if closeErr != nil {
log.Warnf("got an error closing dbus connection, err: %s", closeErr)
}
}
return obj, closeFunc, nil
}

View File

@@ -1,156 +0,0 @@
package dns
import (
"bytes"
"fmt"
"os"
log "github.com/sirupsen/logrus"
)
const (
fileGeneratedResolvConfContentHeader = "# Generated by NetBird"
fileGeneratedResolvConfSearchBeginContent = "search "
fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader +
"\n# If needed you can restore the original file by copying back %s\n\nnameserver %s\n" +
fileGeneratedResolvConfSearchBeginContent + "%s\n"
)
const (
fileDefaultResolvConfBackupLocation = defaultResolvConfPath + ".original.netbird"
fileMaxLineCharsLimit = 256
fileMaxNumberOfSearchDomains = 6
)
var fileSearchLineBeginCharCount = len(fileGeneratedResolvConfSearchBeginContent)
type fileConfigurator struct {
originalPerms os.FileMode
}
func newFileConfigurator() (hostManager, error) {
return &fileConfigurator{}, nil
}
func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error {
backupFileExist := false
_, err := os.Stat(fileDefaultResolvConfBackupLocation)
if err == nil {
backupFileExist = true
}
if !config.routeAll {
if backupFileExist {
err = f.restore()
if err != nil {
return fmt.Errorf("unable to configure DNS for this peer using file manager without a Primary nameserver group. Restoring the original file return err: %s", err)
}
}
return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured")
}
managerType, err := getOSDNSManagerType()
if err != nil {
return err
}
switch managerType {
case fileManager, netbirdManager:
if !backupFileExist {
err = f.backup()
if err != nil {
return fmt.Errorf("unable to backup the resolv.conf file")
}
}
default:
// todo improve this and maybe restart DNS manager from scratch
return fmt.Errorf("something happened and file manager is not your prefered host dns configurator, restart the agent")
}
var searchDomains string
appendedDomains := 0
for _, dConf := range config.domains {
if dConf.matchOnly || dConf.disabled {
continue
}
if appendedDomains >= fileMaxNumberOfSearchDomains {
// lets log all skipped domains
log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, dConf.domain)
continue
}
if fileSearchLineBeginCharCount+len(searchDomains) > fileMaxLineCharsLimit {
// lets log all skipped domains
log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, dConf.domain)
continue
}
searchDomains += " " + dConf.domain
appendedDomains++
}
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains)
err = writeDNSConfig(content, defaultResolvConfPath, f.originalPerms)
if err != nil {
err = f.restore()
if err != nil {
log.Errorf("attempt to restore default file failed with error: %s", err)
}
return err
}
log.Infof("created a NetBird managed %s file with your DNS settings. Added %d search domains. Search list: %s", defaultResolvConfPath, appendedDomains, searchDomains)
return nil
}
func (f *fileConfigurator) restoreHostDNS() error {
return f.restore()
}
func (f *fileConfigurator) backup() error {
stats, err := os.Stat(defaultResolvConfPath)
if err != nil {
return fmt.Errorf("got an error while checking stats for %s file. Error: %s", defaultResolvConfPath, err)
}
f.originalPerms = stats.Mode()
err = copyFile(defaultResolvConfPath, fileDefaultResolvConfBackupLocation)
if err != nil {
return fmt.Errorf("got error while backing up the %s file. Error: %s", defaultResolvConfPath, err)
}
return nil
}
func (f *fileConfigurator) restore() error {
err := copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath)
if err != nil {
return fmt.Errorf("got error while restoring the %s file from %s. Error: %s", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err)
}
return os.RemoveAll(fileDefaultResolvConfBackupLocation)
}
func writeDNSConfig(content, fileName string, permissions os.FileMode) error {
log.Debugf("creating managed file %s", fileName)
var buf bytes.Buffer
buf.WriteString(content)
err := os.WriteFile(fileName, buf.Bytes(), permissions)
if err != nil {
return fmt.Errorf("got an creating resolver file %s. Error: %s", fileName, err)
}
return nil
}
func copyFile(src, dest string) error {
stats, err := os.Stat(src)
if err != nil {
return fmt.Errorf("got an error while checking stats for %s file when copying it. Error: %s", src, err)
}
bytesRead, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("got an error while reading the file %s file for copy. Error: %s", src, err)
}
err = os.WriteFile(dest, bytesRead, stats.Mode())
if err != nil {
return fmt.Errorf("got an writing the destination file %s for copy. Error: %s", dest, err)
}
return nil
}

View File

@@ -1,84 +0,0 @@
package dns
import (
"fmt"
"strings"
nbdns "github.com/netbirdio/netbird/dns"
)
type hostManager interface {
applyDNSConfig(config hostDNSConfig) error
restoreHostDNS() error
}
type hostDNSConfig struct {
domains []domainConfig
routeAll bool
serverIP string
serverPort int
}
type domainConfig struct {
disabled bool
domain string
matchOnly bool
}
type mockHostConfigurator struct {
applyDNSConfigFunc func(config hostDNSConfig) error
restoreHostDNSFunc func() error
}
func (m *mockHostConfigurator) applyDNSConfig(config hostDNSConfig) error {
if m.applyDNSConfigFunc != nil {
return m.applyDNSConfigFunc(config)
}
return fmt.Errorf("method applyDNSSettings is not implemented")
}
func (m *mockHostConfigurator) restoreHostDNS() error {
if m.restoreHostDNSFunc != nil {
return m.restoreHostDNSFunc()
}
return fmt.Errorf("method restoreHostDNS is not implemented")
}
func newNoopHostMocker() hostManager {
return &mockHostConfigurator{
applyDNSConfigFunc: func(config hostDNSConfig) error { return nil },
restoreHostDNSFunc: func() error { return nil },
}
}
func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) hostDNSConfig {
config := hostDNSConfig{
routeAll: false,
serverIP: ip,
serverPort: port,
}
for _, nsConfig := range dnsConfig.NameServerGroups {
if len(nsConfig.NameServers) == 0 {
continue
}
if nsConfig.Primary {
config.routeAll = true
}
for _, domain := range nsConfig.Domains {
config.domains = append(config.domains, domainConfig{
domain: strings.TrimSuffix(domain, "."),
matchOnly: true,
})
}
}
for _, customZone := range dnsConfig.CustomZones {
config.domains = append(config.domains, domainConfig{
domain: strings.TrimSuffix(customZone.Domain, "."),
matchOnly: false,
})
}
return config
}

View File

@@ -1,263 +0,0 @@
package dns
import (
"bufio"
"bytes"
"fmt"
"os/exec"
"strconv"
"strings"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
)
const (
netbirdDNSStateKeyFormat = "State:/Network/Service/NetBird-%s/DNS"
globalIPv4State = "State:/Network/Global/IPv4"
primaryServiceSetupKeyFormat = "Setup:/Network/Service/%s/DNS"
keySupplementalMatchDomains = "SupplementalMatchDomains"
keySupplementalMatchDomainsNoSearch = "SupplementalMatchDomainsNoSearch"
keyServerAddresses = "ServerAddresses"
keyServerPort = "ServerPort"
arraySymbol = "* "
digitSymbol = "# "
scutilPath = "/usr/sbin/scutil"
searchSuffix = "Search"
matchSuffix = "Match"
)
type systemConfigurator struct {
// primaryServiceID primary interface in the system. AKA the interface with the default route
primaryServiceID string
createdKeys map[string]struct{}
}
func newHostManager(_ *iface.WGIface) (hostManager, error) {
return &systemConfigurator{
createdKeys: make(map[string]struct{}),
}, nil
}
func (s *systemConfigurator) applyDNSConfig(config hostDNSConfig) error {
var err error
if config.routeAll {
err = s.addDNSSetupForAll(config.serverIP, config.serverPort)
if err != nil {
return err
}
} else if s.primaryServiceID != "" {
err = s.removeKeyFromSystemConfig(getKeyWithInput(primaryServiceSetupKeyFormat, s.primaryServiceID))
if err != nil {
return err
}
s.primaryServiceID = ""
log.Infof("removed %s:%d as main DNS resolver for this peer", config.serverIP, config.serverPort)
}
var (
searchDomains []string
matchDomains []string
)
for _, dConf := range config.domains {
if dConf.disabled {
continue
}
if dConf.matchOnly {
matchDomains = append(matchDomains, dConf.domain)
continue
}
searchDomains = append(searchDomains, dConf.domain)
}
matchKey := getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)
if len(matchDomains) != 0 {
err = s.addMatchDomains(matchKey, strings.Join(matchDomains, " "), config.serverIP, config.serverPort)
} else {
log.Infof("removing match domains from the system")
err = s.removeKeyFromSystemConfig(matchKey)
}
if err != nil {
return err
}
searchKey := getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix)
if len(searchDomains) != 0 {
err = s.addSearchDomains(searchKey, strings.Join(searchDomains, " "), config.serverIP, config.serverPort)
} else {
log.Infof("removing search domains from the system")
err = s.removeKeyFromSystemConfig(searchKey)
}
if err != nil {
return err
}
return nil
}
func (s *systemConfigurator) restoreHostDNS() error {
lines := ""
for key := range s.createdKeys {
lines += buildRemoveKeyOperation(key)
keyType := "search"
if strings.Contains(key, matchSuffix) {
keyType = "match"
}
log.Infof("removing %s domains from system", keyType)
}
if s.primaryServiceID != "" {
lines += buildRemoveKeyOperation(getKeyWithInput(primaryServiceSetupKeyFormat, s.primaryServiceID))
log.Infof("restoring DNS resolver configuration for system")
}
_, err := runSystemConfigCommand(wrapCommand(lines))
if err != nil {
log.Errorf("got an error while cleaning the system configuration: %s", err)
return err
}
return nil
}
func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error {
line := buildRemoveKeyOperation(key)
_, err := runSystemConfigCommand(wrapCommand(line))
if err != nil {
return err
}
delete(s.createdKeys, key)
return nil
}
func (s *systemConfigurator) addSearchDomains(key, domains string, ip string, port int) error {
err := s.addDNSState(key, domains, ip, port, true)
if err != nil {
return err
}
log.Infof("added %d search domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains)
s.createdKeys[key] = struct{}{}
return nil
}
func (s *systemConfigurator) addMatchDomains(key, domains, dnsServer string, port int) error {
err := s.addDNSState(key, domains, dnsServer, port, false)
if err != nil {
return err
}
log.Infof("added %d match domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains)
s.createdKeys[key] = struct{}{}
return nil
}
func (s *systemConfigurator) addDNSState(state, domains, dnsServer string, port int, enableSearch bool) error {
noSearch := "1"
if enableSearch {
noSearch = "0"
}
lines := buildAddCommandLine(keySupplementalMatchDomains, arraySymbol+domains)
lines += buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+noSearch)
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer)
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
addDomainCommand := buildCreateStateWithOperation(state, lines)
stdinCommands := wrapCommand(addDomainCommand)
_, err := runSystemConfigCommand(stdinCommands)
if err != nil {
return fmt.Errorf("got error while applying state for domains %s, error: %s", domains, err)
}
return nil
}
func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error {
primaryServiceKey := s.getPrimaryService()
if primaryServiceKey == "" {
return fmt.Errorf("couldn't find the primary service key")
}
err := s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port)
if err != nil {
return err
}
log.Infof("configured %s:%d as main DNS resolver for this peer", dnsServer, port)
s.primaryServiceID = primaryServiceKey
return nil
}
func (s *systemConfigurator) getPrimaryService() string {
line := buildCommandLine("show", globalIPv4State, "")
stdinCommands := wrapCommand(line)
b, err := runSystemConfigCommand(stdinCommands)
if err != nil {
log.Error("got error while sending the command: ", err)
return ""
}
scanner := bufio.NewScanner(bytes.NewReader(b))
for scanner.Scan() {
text := scanner.Text()
if strings.Contains(text, "PrimaryService") {
return strings.TrimSpace(strings.Split(text, ":")[1])
}
}
return ""
}
func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int) error {
lines := buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+strconv.Itoa(0))
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer)
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
addDomainCommand := buildCreateStateWithOperation(setupKey, lines)
stdinCommands := wrapCommand(addDomainCommand)
_, err := runSystemConfigCommand(stdinCommands)
if err != nil {
return fmt.Errorf("got error while applying dns setup, error: %s", err)
}
return nil
}
func getKeyWithInput(format, key string) string {
return fmt.Sprintf(format, key)
}
func buildAddCommandLine(key, value string) string {
return buildCommandLine("d.add", key, value)
}
func buildCommandLine(action, key, value string) string {
return fmt.Sprintf("%s %s %s\n", action, key, value)
}
func wrapCommand(commands string) string {
return fmt.Sprintf("open\n%s\nquit\n", commands)
}
func buildRemoveKeyOperation(key string) string {
return fmt.Sprintf("remove %s\n", key)
}
func buildCreateStateWithOperation(state, commands string) string {
return buildWriteStateOperation("set", state, commands)
}
func buildWriteStateOperation(operation, state, commands string) string {
return fmt.Sprintf("d.init\n%s %s\n%s\nset %s\n", operation, state, commands, state)
}
func runSystemConfigCommand(command string) ([]byte, error) {
cmd := exec.Command(scutilPath)
cmd.Stdin = strings.NewReader(command)
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("got error while running system configuration command: \"%s\", error: %s", command, err)
}
return out, nil
}

View File

@@ -1,87 +0,0 @@
package dns
import (
"bufio"
"fmt"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
"os"
"strings"
)
const (
defaultResolvConfPath = "/etc/resolv.conf"
)
const (
netbirdManager osManagerType = iota
fileManager
networkManager
systemdManager
resolvConfManager
)
type osManagerType int
func newHostManager(wgInterface *iface.WGIface) (hostManager, error) {
osManager, err := getOSDNSManagerType()
if err != nil {
return nil, err
}
log.Debugf("discovered mode is: %d", osManager)
switch osManager {
case networkManager:
return newNetworkManagerDbusConfigurator(wgInterface)
case systemdManager:
return newSystemdDbusConfigurator(wgInterface)
case resolvConfManager:
return newResolvConfConfigurator(wgInterface)
default:
return newFileConfigurator()
}
}
func getOSDNSManagerType() (osManagerType, error) {
file, err := os.Open(defaultResolvConfPath)
if err != nil {
return 0, fmt.Errorf("unable to open %s for checking owner, got error: %s", defaultResolvConfPath, err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
if len(text) == 0 {
continue
}
if text[0] != '#' {
return fileManager, nil
}
if strings.Contains(text, fileGeneratedResolvConfContentHeader) {
return netbirdManager, nil
}
if strings.Contains(text, "NetworkManager") && isDbusListenerRunning(networkManagerDest, networkManagerDbusObjectNode) && isNetworkManagerSupported() {
log.Debugf("is nm running on supported v? %t", isNetworkManagerSupportedVersion())
return networkManager, nil
}
if strings.Contains(text, "systemd-resolved") && isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) {
return systemdManager, nil
}
if strings.Contains(text, "resolvconf") {
if isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) {
var value string
err = getSystemdDbusProperty(systemdDbusResolvConfModeProperty, &value)
if err == nil {
if value == systemdDbusResolvConfModeForeign {
return systemdManager, nil
}
}
log.Errorf("got an error while checking systemd resolv conf mode, error: %s", err)
}
return resolvConfManager, nil
}
}
return fileManager, nil
}

View File

@@ -1,264 +0,0 @@
package dns
import (
"fmt"
"strings"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/windows/registry"
)
const (
dnsPolicyConfigMatchPath = "SYSTEM\\CurrentControlSet\\Services\\Dnscache\\Parameters\\DnsPolicyConfig\\NetBird-Match"
dnsPolicyConfigVersionKey = "Version"
dnsPolicyConfigVersionValue = 2
dnsPolicyConfigNameKey = "Name"
dnsPolicyConfigGenericDNSServersKey = "GenericDNSServers"
dnsPolicyConfigConfigOptionsKey = "ConfigOptions"
dnsPolicyConfigConfigOptionsValue = 0x8
)
const (
interfaceConfigPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces"
interfaceConfigNameServerKey = "NameServer"
interfaceConfigSearchListKey = "SearchList"
tcpipParametersPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters"
)
type registryConfigurator struct {
guid string
routingAll bool
existingSearchDomains []string
}
func newHostManager(wgInterface *iface.WGIface) (hostManager, error) {
guid, err := wgInterface.GetInterfaceGUIDString()
if err != nil {
return nil, err
}
return &registryConfigurator{
guid: guid,
}, nil
}
func (r *registryConfigurator) applyDNSConfig(config hostDNSConfig) error {
var err error
if config.routeAll {
err = r.addDNSSetupForAll(config.serverIP)
if err != nil {
return err
}
} else if r.routingAll {
err = r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey)
if err != nil {
return err
}
r.routingAll = false
log.Infof("removed %s as main DNS forwarder for this peer", config.serverIP)
}
var (
searchDomains []string
matchDomains []string
)
for _, dConf := range config.domains {
if dConf.disabled {
continue
}
if !dConf.matchOnly {
searchDomains = append(searchDomains, dConf.domain)
}
matchDomains = append(matchDomains, "."+dConf.domain)
}
if len(matchDomains) != 0 {
err = r.addDNSMatchPolicy(matchDomains, config.serverIP)
} else {
err = removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath)
}
if err != nil {
return err
}
err = r.updateSearchDomains(searchDomains)
if err != nil {
return err
}
return nil
}
func (r *registryConfigurator) addDNSSetupForAll(ip string) error {
err := r.setInterfaceRegistryKeyStringValue(interfaceConfigNameServerKey, ip)
if err != nil {
return fmt.Errorf("adding dns setup for all failed with error: %s", err)
}
r.routingAll = true
log.Infof("configured %s:53 as main DNS forwarder for this peer", ip)
return nil
}
func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) error {
_, err := registry.OpenKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath, registry.QUERY_VALUE)
if err == nil {
err = registry.DeleteKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath)
if err != nil {
return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %s", dnsPolicyConfigMatchPath, err)
}
}
regKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath, registry.SET_VALUE)
if err != nil {
return fmt.Errorf("unable to create registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %s", dnsPolicyConfigMatchPath, err)
}
err = regKey.SetDWordValue(dnsPolicyConfigVersionKey, dnsPolicyConfigVersionValue)
if err != nil {
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigVersionKey, err)
}
err = regKey.SetStringsValue(dnsPolicyConfigNameKey, domains)
if err != nil {
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigNameKey, err)
}
err = regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip)
if err != nil {
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigGenericDNSServersKey, err)
}
err = regKey.SetDWordValue(dnsPolicyConfigConfigOptionsKey, dnsPolicyConfigConfigOptionsValue)
if err != nil {
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigConfigOptionsKey, err)
}
log.Infof("added %d match domains to the state. Domain list: %s", len(domains), domains)
return nil
}
func (r *registryConfigurator) restoreHostDNS() error {
err := removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath)
if err != nil {
log.Error(err)
}
return r.updateSearchDomains([]string{})
}
func (r *registryConfigurator) updateSearchDomains(domains []string) error {
value, err := getLocalMachineRegistryKeyStringValue(tcpipParametersPath, interfaceConfigSearchListKey)
if err != nil {
return fmt.Errorf("unable to get current search domains failed with error: %s", err)
}
valueList := strings.Split(value, ",")
setExisting := false
if len(r.existingSearchDomains) == 0 {
r.existingSearchDomains = valueList
setExisting = true
}
if len(domains) == 0 && setExisting {
log.Infof("added %d search domains to the registry. Domain list: %s", len(domains), domains)
return nil
}
newList := append(r.existingSearchDomains, domains...)
err = setLocalMachineRegistryKeyStringValue(tcpipParametersPath, interfaceConfigSearchListKey, strings.Join(newList, ","))
if err != nil {
return fmt.Errorf("adding search domain failed with error: %s", err)
}
log.Infof("updated the search domains in the registry with %d domains. Domain list: %s", len(domains), domains)
return nil
}
func (r *registryConfigurator) setInterfaceRegistryKeyStringValue(key, value string) error {
regKey, err := r.getInterfaceRegistryKey()
if err != nil {
return err
}
defer regKey.Close()
err = regKey.SetStringValue(key, value)
if err != nil {
return fmt.Errorf("applying key %s with value \"%s\" for interface failed with error: %s", key, value, err)
}
return nil
}
func (r *registryConfigurator) deleteInterfaceRegistryKeyProperty(propertyKey string) error {
regKey, err := r.getInterfaceRegistryKey()
if err != nil {
return err
}
defer regKey.Close()
err = regKey.DeleteValue(propertyKey)
if err != nil {
return fmt.Errorf("deleting registry key %s for interface failed with error: %s", propertyKey, err)
}
return nil
}
func (r *registryConfigurator) getInterfaceRegistryKey() (registry.Key, error) {
var regKey registry.Key
regKeyPath := interfaceConfigPath + "\\" + r.guid
regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.SET_VALUE)
if err != nil {
return regKey, fmt.Errorf("unable to open the interface registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %s", regKeyPath, err)
}
return regKey, nil
}
func removeRegistryKeyFromDNSPolicyConfig(regKeyPath string) error {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.QUERY_VALUE)
if err == nil {
k.Close()
err = registry.DeleteKey(registry.LOCAL_MACHINE, regKeyPath)
if err != nil {
return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %s", regKeyPath, err)
}
}
return nil
}
func getLocalMachineRegistryKeyStringValue(keyPath, key string) (string, error) {
regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.QUERY_VALUE)
if err != nil {
return "", fmt.Errorf("unable to open existing key from registry, key path: HKEY_LOCAL_MACHINE\\%s, error: %s", keyPath, err)
}
defer regKey.Close()
val, _, err := regKey.GetStringValue(key)
if err != nil {
return "", fmt.Errorf("getting %s value for key path HKEY_LOCAL_MACHINE\\%s failed with error: %s", key, keyPath, err)
}
return val, nil
}
func setLocalMachineRegistryKeyStringValue(keyPath, key, value string) error {
regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.SET_VALUE)
if err != nil {
return fmt.Errorf("unable to open existing key from registry, key path: HKEY_LOCAL_MACHINE\\%s, error: %s", keyPath, err)
}
defer regKey.Close()
err = regKey.SetStringValue(key, value)
if err != nil {
return fmt.Errorf("setting %s value %s for key path HKEY_LOCAL_MACHINE\\%s failed with error: %s", key, value, keyPath, err)
}
return nil
}

View File

@@ -1,68 +0,0 @@
package dns
import (
"fmt"
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
log "github.com/sirupsen/logrus"
"sync"
)
type registrationMap map[string]struct{}
type localResolver struct {
registeredMap registrationMap
records sync.Map
}
// ServeDNS handles a DNS request
func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
log.Tracef("received question: %#v\n", r.Question[0])
replyMessage := &dns.Msg{}
replyMessage.SetReply(r)
replyMessage.RecursionAvailable = true
replyMessage.Rcode = dns.RcodeSuccess
response := d.lookupRecord(r)
if response != nil {
replyMessage.Answer = append(replyMessage.Answer, response)
}
err := w.WriteMsg(replyMessage)
if err != nil {
log.Debugf("got an error while writing the local resolver response, error: %v", err)
}
}
func (d *localResolver) lookupRecord(r *dns.Msg) dns.RR {
question := r.Question[0]
record, found := d.records.Load(buildRecordKey(question.Name, question.Qclass, question.Qtype))
if !found {
return nil
}
return record.(dns.RR)
}
func (d *localResolver) registerRecord(record nbdns.SimpleRecord) error {
fullRecord, err := dns.NewRR(record.String())
if err != nil {
return err
}
fullRecord.Header().Rdlength = record.Len()
header := fullRecord.Header()
d.records.Store(buildRecordKey(header.Name, header.Class, header.Rrtype), fullRecord)
return nil
}
func (d *localResolver) deleteRecord(recordKey string) {
d.records.Delete(dns.Fqdn(recordKey))
}
func buildRecordKey(name string, class, qType uint16) string {
key := fmt.Sprintf("%s_%d_%d", name, class, qType)
return key
}

View File

@@ -1,86 +0,0 @@
package dns
import (
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
"strings"
"testing"
)
func TestLocalResolver_ServeDNS(t *testing.T) {
recordA := nbdns.SimpleRecord{
Name: "peera.netbird.cloud.",
Type: 1,
Class: nbdns.DefaultClass,
TTL: 300,
RData: "1.2.3.4",
}
recordCNAME := nbdns.SimpleRecord{
Name: "peerb.netbird.cloud.",
Type: 5,
Class: nbdns.DefaultClass,
TTL: 300,
RData: "www.netbird.io",
}
testCases := []struct {
name string
inputRecord nbdns.SimpleRecord
inputMSG *dns.Msg
responseShouldBeNil bool
}{
{
name: "Should Resolve A Record",
inputRecord: recordA,
inputMSG: new(dns.Msg).SetQuestion(recordA.Name, dns.TypeA),
},
{
name: "Should Resolve CNAME Record",
inputRecord: recordCNAME,
inputMSG: new(dns.Msg).SetQuestion(recordCNAME.Name, dns.TypeCNAME),
},
{
name: "Should Not Write When Not Found A Record",
inputRecord: recordA,
inputMSG: new(dns.Msg).SetQuestion("not.found.com", dns.TypeA),
responseShouldBeNil: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
resolver := &localResolver{
registeredMap: make(registrationMap),
}
_ = resolver.registerRecord(testCase.inputRecord)
var responseMSG *dns.Msg
responseWriter := &mockResponseWriter{
WriteMsgFunc: func(m *dns.Msg) error {
responseMSG = m
return nil
},
}
resolver.ServeDNS(responseWriter, testCase.inputMSG)
if responseMSG == nil || len(responseMSG.Answer) == 0 {
if testCase.responseShouldBeNil {
return
}
t.Fatalf("should write a response message")
}
answerString := responseMSG.Answer[0].String()
if !strings.Contains(answerString, testCase.inputRecord.Name) {
t.Fatalf("answer doesn't contain the same domain name: \nWant: %s\nGot:%s", testCase.name, answerString)
}
if !strings.Contains(answerString, dns.Type(testCase.inputRecord.Type).String()) {
t.Fatalf("answer doesn't contain the correct type: \nWant: %s\nGot:%s", dns.Type(testCase.inputRecord.Type).String(), answerString)
}
if !strings.Contains(answerString, testCase.inputRecord.RData) {
t.Fatalf("answer doesn't contain the same address: \nWant: %s\nGot:%s", testCase.inputRecord.RData, answerString)
}
})
}
}

View File

@@ -1,35 +0,0 @@
package dns
import (
"fmt"
nbdns "github.com/netbirdio/netbird/dns"
)
// MockServer is the mock instance of a dns server
type MockServer struct {
StartFunc func()
StopFunc func()
UpdateDNSServerFunc func(serial uint64, update nbdns.Config) error
}
// Start mock implementation of Start from Server interface
func (m *MockServer) Start() {
if m.StartFunc != nil {
m.StartFunc()
}
}
// Stop mock implementation of Stop from Server interface
func (m *MockServer) Stop() {
if m.StopFunc != nil {
m.StopFunc()
}
}
// UpdateDNSServer mock implementation of UpdateDNSServer from Server interface
func (m *MockServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
if m.UpdateDNSServerFunc != nil {
return m.UpdateDNSServerFunc(serial, update)
}
return fmt.Errorf("method UpdateDNSServer is not implemented")
}

View File

@@ -1,26 +0,0 @@
package dns
import (
"net"
"github.com/miekg/dns"
)
type mockResponseWriter struct {
WriteMsgFunc func(m *dns.Msg) error
}
func (rw *mockResponseWriter) WriteMsg(m *dns.Msg) error {
if rw.WriteMsgFunc != nil {
return rw.WriteMsgFunc(m)
}
return nil
}
func (rw *mockResponseWriter) LocalAddr() net.Addr { return nil }
func (rw *mockResponseWriter) RemoteAddr() net.Addr { return nil }
func (rw *mockResponseWriter) Write([]byte) (int, error) { return 0, nil }
func (rw *mockResponseWriter) Close() error { return nil }
func (rw *mockResponseWriter) TsigStatus() error { return nil }
func (rw *mockResponseWriter) TsigTimersOnly(bool) {}
func (rw *mockResponseWriter) Hijack() {}

View File

@@ -1,302 +0,0 @@
package dns
import (
"context"
"encoding/binary"
"fmt"
"net/netip"
"regexp"
"time"
"github.com/godbus/dbus/v5"
"github.com/hashicorp/go-version"
"github.com/miekg/dns"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
)
const (
networkManagerDest = "org.freedesktop.NetworkManager"
networkManagerDbusObjectNode = "/org/freedesktop/NetworkManager"
networkManagerDbusDNSManagerInterface = "org.freedesktop.NetworkManager.DnsManager"
networkManagerDbusDNSManagerObjectNode = networkManagerDbusObjectNode + "/DnsManager"
networkManagerDbusDNSManagerModeProperty = networkManagerDbusDNSManagerInterface + ".Mode"
networkManagerDbusDNSManagerRcManagerProperty = networkManagerDbusDNSManagerInterface + ".RcManager"
networkManagerDbusVersionProperty = "org.freedesktop.NetworkManager.Version"
networkManagerDbusGetDeviceByIPIfaceMethod = networkManagerDest + ".GetDeviceByIpIface"
networkManagerDbusDeviceInterface = "org.freedesktop.NetworkManager.Device"
networkManagerDbusDeviceGetAppliedConnectionMethod = networkManagerDbusDeviceInterface + ".GetAppliedConnection"
networkManagerDbusDeviceReapplyMethod = networkManagerDbusDeviceInterface + ".Reapply"
networkManagerDbusDeviceDeleteMethod = networkManagerDbusDeviceInterface + ".Delete"
networkManagerDbusDefaultBehaviorFlag networkManagerConfigBehavior = 0
networkManagerDbusIPv4Key = "ipv4"
networkManagerDbusIPv6Key = "ipv6"
networkManagerDbusDNSKey = "dns"
networkManagerDbusDNSSearchKey = "dns-search"
networkManagerDbusDNSPriorityKey = "dns-priority"
// dns priority doc https://wiki.gnome.org/Projects/NetworkManager/DNS
networkManagerDbusPrimaryDNSPriority int32 = -500
networkManagerDbusWithMatchDomainPriority int32 = 0
networkManagerDbusSearchDomainOnlyPriority int32 = 50
supportedNetworkManagerVersionConstraint = ">= 1.16, < 1.28"
)
type networkManagerDbusConfigurator struct {
dbusLinkObject dbus.ObjectPath
routingAll bool
}
// the types below are based on dbus specification, each field is mapped to a dbus type
// see https://dbus.freedesktop.org/doc/dbus-specification.html#basic-types for more details on dbus types
// see https://networkmanager.dev/docs/api/latest/gdbus-org.freedesktop.NetworkManager.Device.html on Network Manager input types
// networkManagerConnSettings maps to a (a{sa{sv}}) dbus output from GetAppliedConnection and input for Reapply methods
type networkManagerConnSettings map[string]map[string]dbus.Variant
// networkManagerConfigVersion maps to a (t) dbus output from GetAppliedConnection and input for Reapply methods
type networkManagerConfigVersion uint64
// networkManagerConfigBehavior maps to a (u) dbus input for GetAppliedConnection and Reapply methods
type networkManagerConfigBehavior uint32
// cleanDeprecatedSettings cleans deprecated settings that still returned by
// the GetAppliedConnection methods but can't be reApplied
func (s networkManagerConnSettings) cleanDeprecatedSettings() {
for _, key := range []string{"addresses", "routes"} {
delete(s[networkManagerDbusIPv4Key], key)
delete(s[networkManagerDbusIPv6Key], key)
}
}
func newNetworkManagerDbusConfigurator(wgInterface *iface.WGIface) (hostManager, error) {
obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusObjectNode)
if err != nil {
return nil, err
}
defer closeConn()
var s string
err = obj.Call(networkManagerDbusGetDeviceByIPIfaceMethod, dbusDefaultFlag, wgInterface.Name()).Store(&s)
if err != nil {
return nil, err
}
log.Debugf("got network manager dbus Link Object: %s from net interface %s", s, wgInterface.Name())
return &networkManagerDbusConfigurator{
dbusLinkObject: dbus.ObjectPath(s),
}, nil
}
func (n *networkManagerDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
connSettings, configVersion, err := n.getAppliedConnectionSettings()
if err != nil {
return fmt.Errorf("got an error while retrieving the applied connection settings, error: %s", err)
}
connSettings.cleanDeprecatedSettings()
dnsIP, err := netip.ParseAddr(config.serverIP)
if err != nil {
return fmt.Errorf("unable to parse ip address, error: %s", err)
}
convDNSIP := binary.LittleEndian.Uint32(dnsIP.AsSlice())
connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSKey] = dbus.MakeVariant([]uint32{convDNSIP})
var (
searchDomains []string
matchDomains []string
)
for _, dConf := range config.domains {
if dConf.disabled {
continue
}
if dConf.matchOnly {
matchDomains = append(matchDomains, "~."+dns.Fqdn(dConf.domain))
continue
}
searchDomains = append(searchDomains, dns.Fqdn(dConf.domain))
}
newDomainList := append(searchDomains, matchDomains...)
priority := networkManagerDbusSearchDomainOnlyPriority
switch {
case config.routeAll:
priority = networkManagerDbusPrimaryDNSPriority
newDomainList = append(newDomainList, "~.")
if !n.routingAll {
log.Infof("configured %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
}
case len(matchDomains) > 0:
priority = networkManagerDbusWithMatchDomainPriority
}
if priority != networkManagerDbusPrimaryDNSPriority && n.routingAll {
log.Infof("removing %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
n.routingAll = false
}
connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSPriorityKey] = dbus.MakeVariant(priority)
connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSSearchKey] = dbus.MakeVariant(newDomainList)
log.Infof("adding %d search domains and %d match domains. Search list: %s , Match list: %s", len(searchDomains), len(matchDomains), searchDomains, matchDomains)
err = n.reApplyConnectionSettings(connSettings, configVersion)
if err != nil {
return fmt.Errorf("got an error while reapplying the connection with new settings, error: %s", err)
}
return nil
}
func (n *networkManagerDbusConfigurator) restoreHostDNS() error {
// once the interface is gone network manager cleans all config associated with it
return n.deleteConnectionSettings()
}
func (n *networkManagerDbusConfigurator) getAppliedConnectionSettings() (networkManagerConnSettings, networkManagerConfigVersion, error) {
obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject)
if err != nil {
return nil, 0, fmt.Errorf("got error while attempting to retrieve the applied connection settings, err: %s", err)
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
var (
connSettings networkManagerConnSettings
configVersion networkManagerConfigVersion
)
err = obj.CallWithContext(ctx, networkManagerDbusDeviceGetAppliedConnectionMethod, dbusDefaultFlag,
networkManagerDbusDefaultBehaviorFlag).Store(&connSettings, &configVersion)
if err != nil {
return nil, 0, fmt.Errorf("got error while calling GetAppliedConnection method with context, err: %s", err)
}
return connSettings, configVersion, nil
}
func (n *networkManagerDbusConfigurator) reApplyConnectionSettings(connSettings networkManagerConnSettings, configVersion networkManagerConfigVersion) error {
obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject)
if err != nil {
return fmt.Errorf("got error while attempting to retrieve the applied connection settings, err: %s", err)
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
err = obj.CallWithContext(ctx, networkManagerDbusDeviceReapplyMethod, dbusDefaultFlag,
connSettings, configVersion, networkManagerDbusDefaultBehaviorFlag).Store()
if err != nil {
return fmt.Errorf("got error while calling ReApply method with context, err: %s", err)
}
return nil
}
func (n *networkManagerDbusConfigurator) deleteConnectionSettings() error {
obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject)
if err != nil {
return fmt.Errorf("got error while attempting to retrieve the applied connection settings, err: %s", err)
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
err = obj.CallWithContext(ctx, networkManagerDbusDeviceDeleteMethod, dbusDefaultFlag).Store()
if err != nil {
return fmt.Errorf("got error while calling delete method with context, err: %s", err)
}
return nil
}
func isNetworkManagerSupported() bool {
return isNetworkManagerSupportedVersion() && isNetworkManagerSupportedMode()
}
func isNetworkManagerSupportedMode() bool {
var mode string
err := getNetworkManagerDNSProperty(networkManagerDbusDNSManagerModeProperty, &mode)
if err != nil {
log.Error(err)
return false
}
switch mode {
case "dnsmasq", "unbound", "systemd-resolved":
return true
default:
var rcManager string
err = getNetworkManagerDNSProperty(networkManagerDbusDNSManagerRcManagerProperty, &rcManager)
if err != nil {
log.Error(err)
return false
}
if rcManager == "unmanaged" {
return false
}
}
return true
}
func getNetworkManagerDNSProperty(property string, store any) error {
obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusDNSManagerObjectNode)
if err != nil {
return fmt.Errorf("got error while attempting to retrieve the network manager dns manager object, error: %s", err)
}
defer closeConn()
v, e := obj.GetProperty(property)
if e != nil {
return fmt.Errorf("got an error getting property %s: %v", property, e)
}
return v.Store(store)
}
func isNetworkManagerSupportedVersion() bool {
obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusObjectNode)
if err != nil {
log.Errorf("got error while attempting to get the network manager object, err: %s", err)
return false
}
defer closeConn()
value, err := obj.GetProperty(networkManagerDbusVersionProperty)
if err != nil {
log.Errorf("unable to retrieve network manager mode, got error: %s", err)
return false
}
versionValue, err := parseVersion(value.Value().(string))
if err != nil {
return false
}
constraints, err := version.NewConstraint(supportedNetworkManagerVersionConstraint)
if err != nil {
return false
}
return constraints.Check(versionValue)
}
func parseVersion(inputVersion string) (*version.Version, error) {
reg, err := regexp.Compile(version.SemverRegexpRaw)
if err != nil {
return nil, err
}
if inputVersion == "" || !reg.MatchString(inputVersion) {
return nil, fmt.Errorf("couldn't parse the provided version: Not SemVer")
}
verObj, err := version.NewVersion(inputVersion)
if err != nil {
return nil, err
}
return verObj, nil
}

View File

@@ -1,85 +0,0 @@
package dns
import (
"fmt"
"os/exec"
"strings"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
)
const resolvconfCommand = "resolvconf"
type resolvconf struct {
ifaceName string
}
func newResolvConfConfigurator(wgInterface *iface.WGIface) (hostManager, error) {
return &resolvconf{
ifaceName: wgInterface.Name(),
}, nil
}
func (r *resolvconf) applyDNSConfig(config hostDNSConfig) error {
var err error
if !config.routeAll {
err = r.restoreHostDNS()
if err != nil {
log.Error(err)
}
return fmt.Errorf("unable to configure DNS for this peer using resolvconf manager without a nameserver group with all domains configured")
}
var searchDomains string
appendedDomains := 0
for _, dConf := range config.domains {
if dConf.matchOnly || dConf.disabled {
continue
}
if appendedDomains >= fileMaxNumberOfSearchDomains {
// lets log all skipped domains
log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, dConf.domain)
continue
}
if fileSearchLineBeginCharCount+len(searchDomains) > fileMaxLineCharsLimit {
// lets log all skipped domains
log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, dConf.domain)
continue
}
searchDomains += " " + dConf.domain
appendedDomains++
}
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains)
err = r.applyConfig(content)
if err != nil {
return err
}
log.Infof("added %d search domains. Search list: %s", appendedDomains, searchDomains)
return nil
}
func (r *resolvconf) restoreHostDNS() error {
cmd := exec.Command(resolvconfCommand, "-f", "-d", r.ifaceName)
_, err := cmd.Output()
if err != nil {
return fmt.Errorf("got an error while removing resolvconf configuration for %s interface, error: %s", r.ifaceName, err)
}
return nil
}
func (r *resolvconf) applyConfig(content string) error {
cmd := exec.Command(resolvconfCommand, "-x", "-a", r.ifaceName)
cmd.Stdin = strings.NewReader(content)
_, err := cmd.Output()
if err != nil {
return fmt.Errorf("got an error while appying resolvconf configuration for %s interface, error: %s", r.ifaceName, err)
}
return nil
}

View File

@@ -1,10 +0,0 @@
package dns
import nbdns "github.com/netbirdio/netbird/dns"
// Server is a dns server interface
type Server interface {
Start()
Stop()
UpdateDNSServer(serial uint64, update nbdns.Config) error
}

View File

@@ -1,32 +0,0 @@
package dns
import (
"context"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
)
// DefaultServer dummy dns server
type DefaultServer struct {
}
// NewDefaultServer On Android the DNS feature is not supported yet
func NewDefaultServer(ctx context.Context, wgInterface *iface.WGIface, customAddress string) (*DefaultServer, error) {
return &DefaultServer{}, nil
}
// Start dummy implementation
func (s DefaultServer) Start() {
}
// Stop dummy implementation
func (s DefaultServer) Stop() {
}
// UpdateDNSServer dummy implementation
func (s DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
return nil
}

View File

@@ -1,465 +0,0 @@
//go:build !android
package dns
import (
"context"
"fmt"
"net"
"net/netip"
"runtime"
"sync"
"time"
"github.com/miekg/dns"
"github.com/mitchellh/hashstructure/v2"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
)
const (
defaultPort = 53
customPort = 5053
defaultIP = "127.0.0.1"
customIP = "127.0.0.153"
)
// DefaultServer dns server object
type DefaultServer struct {
ctx context.Context
ctxCancel context.CancelFunc
upstreamCtxCancel context.CancelFunc
mux sync.Mutex
server *dns.Server
dnsMux *dns.ServeMux
dnsMuxMap registrationMap
localResolver *localResolver
wgInterface *iface.WGIface
hostManager hostManager
updateSerial uint64
listenerIsRunning bool
runtimePort int
runtimeIP string
previousConfigHash uint64
currentConfig hostDNSConfig
customAddress *netip.AddrPort
}
type muxUpdate struct {
domain string
handler dns.Handler
}
// NewDefaultServer returns a new dns server
func NewDefaultServer(ctx context.Context, wgInterface *iface.WGIface, customAddress string) (*DefaultServer, error) {
mux := dns.NewServeMux()
dnsServer := &dns.Server{
Net: "udp",
Handler: mux,
UDPSize: 65535,
}
ctx, stop := context.WithCancel(ctx)
var addrPort *netip.AddrPort
if customAddress != "" {
parsedAddrPort, err := netip.ParseAddrPort(customAddress)
if err != nil {
stop()
return nil, fmt.Errorf("unable to parse the custom dns address, got error: %s", err)
}
addrPort = &parsedAddrPort
}
defaultServer := &DefaultServer{
ctx: ctx,
ctxCancel: stop,
server: dnsServer,
dnsMux: mux,
dnsMuxMap: make(registrationMap),
localResolver: &localResolver{
registeredMap: make(registrationMap),
},
wgInterface: wgInterface,
runtimePort: defaultPort,
customAddress: addrPort,
}
hostmanager, err := newHostManager(wgInterface)
if err != nil {
stop()
return nil, err
}
defaultServer.hostManager = hostmanager
return defaultServer, err
}
// Start runs the listener in a go routine
func (s *DefaultServer) Start() {
if s.customAddress != nil {
s.runtimeIP = s.customAddress.Addr().String()
s.runtimePort = int(s.customAddress.Port())
} else {
ip, port, err := s.getFirstListenerAvailable()
if err != nil {
log.Error(err)
return
}
s.runtimeIP = ip
s.runtimePort = port
}
s.server.Addr = fmt.Sprintf("%s:%d", s.runtimeIP, s.runtimePort)
log.Debugf("starting dns on %s", s.server.Addr)
go func() {
s.setListenerStatus(true)
defer s.setListenerStatus(false)
err := s.server.ListenAndServe()
if err != nil {
log.Errorf("dns server running with %d port returned an error: %v. Will not retry", s.runtimePort, err)
}
}()
}
func (s *DefaultServer) getFirstListenerAvailable() (string, int, error) {
ips := []string{defaultIP, customIP}
if runtime.GOOS != "darwin" && s.wgInterface != nil {
ips = append([]string{s.wgInterface.Address().IP.String()}, ips...)
}
ports := []int{defaultPort, customPort}
for _, port := range ports {
for _, ip := range ips {
addrString := fmt.Sprintf("%s:%d", ip, port)
udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(addrString))
probeListener, err := net.ListenUDP("udp", udpAddr)
if err == nil {
err = probeListener.Close()
if err != nil {
log.Errorf("got an error closing the probe listener, error: %s", err)
}
return ip, port, nil
}
log.Warnf("binding dns on %s is not available, error: %s", addrString, err)
}
}
return "", 0, fmt.Errorf("unable to find an unused ip and port combination. IPs tested: %v and ports %v", ips, ports)
}
func (s *DefaultServer) setListenerStatus(running bool) {
s.listenerIsRunning = running
}
// Stop stops the server
func (s *DefaultServer) Stop() {
s.mux.Lock()
defer s.mux.Unlock()
s.ctxCancel()
err := s.hostManager.restoreHostDNS()
if err != nil {
log.Error(err)
}
err = s.stopListener()
if err != nil {
log.Error(err)
}
}
func (s *DefaultServer) stopListener() error {
if !s.listenerIsRunning {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := s.server.ShutdownContext(ctx)
if err != nil {
return fmt.Errorf("stopping dns server listener returned an error: %v", err)
}
return nil
}
// UpdateDNSServer processes an update received from the management service
func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
select {
case <-s.ctx.Done():
log.Infof("not updating DNS server as context is closed")
return s.ctx.Err()
default:
if serial < s.updateSerial {
return fmt.Errorf("not applying dns update, error: "+
"network update is %d behind the last applied update", s.updateSerial-serial)
}
s.mux.Lock()
defer s.mux.Unlock()
hash, err := hashstructure.Hash(update, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
IgnoreZeroValue: true,
SlicesAsSets: true,
UseStringer: true,
})
if err != nil {
log.Errorf("unable to hash the dns configuration update, got error: %s", err)
}
if s.previousConfigHash == hash {
log.Debugf("not applying the dns configuration update as there is nothing new")
s.updateSerial = serial
return nil
}
if err := s.applyConfiguration(update); err != nil {
return err
}
s.updateSerial = serial
s.previousConfigHash = hash
return nil
}
}
func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
// is the service should be disabled, we stop the listener
// and proceed with a regular update to clean up the handlers and records
if !update.ServiceEnable {
err := s.stopListener()
if err != nil {
log.Error(err)
}
} else if !s.listenerIsRunning {
s.Start()
}
localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones)
if err != nil {
return fmt.Errorf("not applying dns update, error: %v", err)
}
upstreamMuxUpdates, err := s.buildUpstreamHandlerUpdate(update.NameServerGroups)
if err != nil {
return fmt.Errorf("not applying dns update, error: %v", err)
}
muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...)
s.updateMux(muxUpdates)
s.updateLocalResolver(localRecords)
s.currentConfig = dnsConfigToHostDNSConfig(update, s.runtimeIP, s.runtimePort)
if err = s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
log.Error(err)
}
return nil
}
func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]muxUpdate, map[string]nbdns.SimpleRecord, error) {
var muxUpdates []muxUpdate
localRecords := make(map[string]nbdns.SimpleRecord, 0)
for _, customZone := range customZones {
if len(customZone.Records) == 0 {
return nil, nil, fmt.Errorf("received an empty list of records")
}
muxUpdates = append(muxUpdates, muxUpdate{
domain: customZone.Domain,
handler: s.localResolver,
})
for _, record := range customZone.Records {
var class uint16 = dns.ClassINET
if record.Class != nbdns.DefaultClass {
return nil, nil, fmt.Errorf("received an invalid class type: %s", record.Class)
}
key := buildRecordKey(record.Name, class, uint16(record.Type))
localRecords[key] = record
}
}
return muxUpdates, localRecords, nil
}
func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.NameServerGroup) ([]muxUpdate, error) {
// clean up the previous upstream resolver
if s.upstreamCtxCancel != nil {
s.upstreamCtxCancel()
}
var muxUpdates []muxUpdate
for _, nsGroup := range nameServerGroups {
if len(nsGroup.NameServers) == 0 {
log.Warn("received a nameserver group with empty nameserver list")
continue
}
var ctx context.Context
ctx, s.upstreamCtxCancel = context.WithCancel(s.ctx)
handler := newUpstreamResolver(ctx)
for _, ns := range nsGroup.NameServers {
if ns.NSType != nbdns.UDPNameServerType {
log.Warnf("skiping nameserver %s with type %s, this peer supports only %s",
ns.IP.String(), ns.NSType.String(), nbdns.UDPNameServerType.String())
continue
}
handler.upstreamServers = append(handler.upstreamServers, getNSHostPort(ns))
}
if len(handler.upstreamServers) == 0 {
log.Errorf("received a nameserver group with an invalid nameserver list")
continue
}
// when upstream fails to resolve domain several times over all it servers
// it will calls this hook to exclude self from the configuration and
// reapply DNS settings, but it not touch the original configuration and serial number
// because it is temporal deactivation until next try
//
// after some period defined by upstream it trys to reactivate self by calling this hook
// everything we need here is just to re-apply current configuration because it already
// contains this upstream settings (temporal deactivation not removed it)
handler.deactivate, handler.reactivate = s.upstreamCallbacks(nsGroup, handler)
if nsGroup.Primary {
muxUpdates = append(muxUpdates, muxUpdate{
domain: nbdns.RootZone,
handler: handler,
})
continue
}
if len(nsGroup.Domains) == 0 {
return nil, fmt.Errorf("received a non primary nameserver group with an empty domain list")
}
for _, domain := range nsGroup.Domains {
if domain == "" {
return nil, fmt.Errorf("received a nameserver group with an empty domain element")
}
muxUpdates = append(muxUpdates, muxUpdate{
domain: domain,
handler: handler,
})
}
}
return muxUpdates, nil
}
func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) {
muxUpdateMap := make(registrationMap)
for _, update := range muxUpdates {
s.registerMux(update.domain, update.handler)
muxUpdateMap[update.domain] = struct{}{}
}
for key := range s.dnsMuxMap {
_, found := muxUpdateMap[key]
if !found {
s.deregisterMux(key)
}
}
s.dnsMuxMap = muxUpdateMap
}
func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) {
for key := range s.localResolver.registeredMap {
_, found := update[key]
if !found {
s.localResolver.deleteRecord(key)
}
}
updatedMap := make(registrationMap)
for key, record := range update {
err := s.localResolver.registerRecord(record)
if err != nil {
log.Warnf("got an error while registering the record (%s), error: %v", record.String(), err)
}
updatedMap[key] = struct{}{}
}
s.localResolver.registeredMap = updatedMap
}
func getNSHostPort(ns nbdns.NameServer) string {
return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port)
}
func (s *DefaultServer) registerMux(pattern string, handler dns.Handler) {
s.dnsMux.Handle(pattern, handler)
}
func (s *DefaultServer) deregisterMux(pattern string) {
s.dnsMux.HandleRemove(pattern)
}
// upstreamCallbacks returns two functions, the first one is used to deactivate
// the upstream resolver from the configuration, the second one is used to
// reactivate it. Not allowed to call reactivate before deactivate.
func (s *DefaultServer) upstreamCallbacks(
nsGroup *nbdns.NameServerGroup,
handler dns.Handler,
) (deactivate func(), reactivate func()) {
var removeIndex map[string]int
deactivate = func() {
s.mux.Lock()
defer s.mux.Unlock()
l := log.WithField("nameservers", nsGroup.NameServers)
l.Info("temporary deactivate nameservers group due timeout")
removeIndex = make(map[string]int)
for _, domain := range nsGroup.Domains {
removeIndex[domain] = -1
}
if nsGroup.Primary {
removeIndex[nbdns.RootZone] = -1
s.currentConfig.routeAll = false
}
for i, item := range s.currentConfig.domains {
if _, found := removeIndex[item.domain]; found {
s.currentConfig.domains[i].disabled = true
s.deregisterMux(item.domain)
removeIndex[item.domain] = i
}
}
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
l.WithError(err).Error("fail to apply nameserver deactivation on the host")
}
}
reactivate = func() {
s.mux.Lock()
defer s.mux.Unlock()
for domain, i := range removeIndex {
if i == -1 || i >= len(s.currentConfig.domains) || s.currentConfig.domains[i].domain != domain {
continue
}
s.currentConfig.domains[i].disabled = false
s.registerMux(domain, handler)
}
l := log.WithField("nameservers", nsGroup.NameServers)
l.Debug("reactivate temporary disabled nameserver group")
if nsGroup.Primary {
s.currentConfig.routeAll = true
}
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
l.WithError(err).Error("reactivate temporary disabled nameserver group, DNS update apply")
}
}
return
}

View File

@@ -1,440 +0,0 @@
package dns
import (
"context"
"fmt"
"net"
"net/netip"
"strings"
"testing"
"time"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
)
var zoneRecords = []nbdns.SimpleRecord{
{
Name: "peera.netbird.cloud",
Type: 1,
Class: nbdns.DefaultClass,
TTL: 300,
RData: "1.2.3.4",
},
}
func TestUpdateDNSServer(t *testing.T) {
nameServers := []nbdns.NameServer{
{
IP: netip.MustParseAddr("8.8.8.8"),
NSType: nbdns.UDPNameServerType,
Port: 53,
},
{
IP: netip.MustParseAddr("8.8.4.4"),
NSType: nbdns.UDPNameServerType,
Port: 53,
},
}
testCases := []struct {
name string
initUpstreamMap registrationMap
initLocalMap registrationMap
initSerial uint64
inputSerial uint64
inputUpdate nbdns.Config
shouldFail bool
expectedUpstreamMap registrationMap
expectedLocalMap registrationMap
}{
{
name: "Initial Config Should Succeed",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []*nbdns.NameServerGroup{
{
Domains: []string{"netbird.io"},
NameServers: nameServers,
},
{
NameServers: nameServers,
Primary: true,
},
},
},
expectedUpstreamMap: registrationMap{"netbird.io": struct{}{}, "netbird.cloud": struct{}{}, nbdns.RootZone: struct{}{}},
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
},
{
name: "New Config Should Succeed",
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
initUpstreamMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []*nbdns.NameServerGroup{
{
Domains: []string{"netbird.io"},
NameServers: nameServers,
},
},
},
expectedUpstreamMap: registrationMap{"netbird.io": struct{}{}, "netbird.cloud": struct{}{}},
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
},
{
name: "Smaller Config Serial Should Be Skipped",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 2,
inputSerial: 1,
shouldFail: true,
},
{
name: "Empty NS Group Domain Or Not Primary Element Should Fail",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []*nbdns.NameServerGroup{
{
NameServers: nameServers,
},
},
},
shouldFail: true,
},
{
name: "Invalid NS Group Nameservers list Should Fail",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []*nbdns.NameServerGroup{
{
NameServers: nameServers,
},
},
},
shouldFail: true,
},
{
name: "Invalid Custom Zone Records list Should Fail",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
},
},
NameServerGroups: []*nbdns.NameServerGroup{
{
NameServers: nameServers,
Primary: true,
},
},
},
shouldFail: true,
},
{
name: "Empty Config Should Succeed and Clean Maps",
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
initUpstreamMap: registrationMap{zoneRecords[0].Name: struct{}{}},
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{ServiceEnable: true},
expectedUpstreamMap: make(registrationMap),
expectedLocalMap: make(registrationMap),
},
{
name: "Disabled Service Should clean map",
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
initUpstreamMap: registrationMap{zoneRecords[0].Name: struct{}{}},
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{ServiceEnable: false},
expectedUpstreamMap: make(registrationMap),
expectedLocalMap: make(registrationMap),
},
}
for n, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
newNet, err := stdnet.NewNet(nil)
if err != nil {
t.Fatal(err)
}
wgIface, err := iface.NewWGIFace(fmt.Sprintf("utun230%d", n), fmt.Sprintf("100.66.100.%d/32", n+1), iface.DefaultMTU, nil, nil, newNet)
if err != nil {
t.Fatal(err)
}
err = wgIface.Create()
if err != nil {
t.Fatal(err)
}
defer func() {
err = wgIface.Close()
if err != nil {
t.Log(err)
}
}()
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "")
if err != nil {
t.Fatal(err)
}
defer func() {
err = dnsServer.hostManager.restoreHostDNS()
if err != nil {
t.Log(err)
}
}()
dnsServer.dnsMuxMap = testCase.initUpstreamMap
dnsServer.localResolver.registeredMap = testCase.initLocalMap
dnsServer.updateSerial = testCase.initSerial
// pretend we are running
dnsServer.listenerIsRunning = true
err = dnsServer.UpdateDNSServer(testCase.inputSerial, testCase.inputUpdate)
if err != nil {
if testCase.shouldFail {
return
}
t.Fatalf("update dns server should not fail, got error: %v", err)
}
if len(dnsServer.dnsMuxMap) != len(testCase.expectedUpstreamMap) {
t.Fatalf("update upstream failed, map size is different than expected, want %d, got %d", len(testCase.expectedUpstreamMap), len(dnsServer.dnsMuxMap))
}
for key := range testCase.expectedUpstreamMap {
_, found := dnsServer.dnsMuxMap[key]
if !found {
t.Fatalf("update upstream failed, key %s was not found in the dnsMuxMap: %#v", key, dnsServer.dnsMuxMap)
}
}
if len(dnsServer.localResolver.registeredMap) != len(testCase.expectedLocalMap) {
t.Fatalf("update local failed, registered map size is different than expected, want %d, got %d", len(testCase.expectedLocalMap), len(dnsServer.localResolver.registeredMap))
}
for key := range testCase.expectedLocalMap {
_, found := dnsServer.localResolver.registeredMap[key]
if !found {
t.Fatalf("update local failed, key %s was not found in the localResolver.registeredMap: %#v", key, dnsServer.localResolver.registeredMap)
}
}
})
}
}
func TestDNSServerStartStop(t *testing.T) {
testCases := []struct {
name string
addrPort string
}{
{
name: "Should Pass With Port Discovery",
},
{
name: "Should Pass With Custom Port",
addrPort: "127.0.0.1:3535",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
dnsServer := getDefaultServerWithNoHostManager(t, testCase.addrPort)
dnsServer.hostManager = newNoopHostMocker()
dnsServer.Start()
time.Sleep(100 * time.Millisecond)
if !dnsServer.listenerIsRunning {
t.Fatal("dns server listener is not running")
}
defer dnsServer.Stop()
err := dnsServer.localResolver.registerRecord(zoneRecords[0])
if err != nil {
t.Error(err)
}
dnsServer.dnsMux.Handle("netbird.cloud", dnsServer.localResolver)
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: time.Second * 5,
}
addr := fmt.Sprintf("%s:%d", dnsServer.runtimeIP, dnsServer.runtimePort)
conn, err := d.DialContext(ctx, network, addr)
if err != nil {
t.Log(err)
// retry test before exit, for slower systems
return d.DialContext(ctx, network, addr)
}
return conn, nil
},
}
ips, err := resolver.LookupHost(context.Background(), zoneRecords[0].Name)
if err != nil {
t.Fatalf("failed to connect to the server, error: %v", err)
}
if ips[0] != zoneRecords[0].RData {
t.Fatalf("got a different IP from the server: want %s, got %s", zoneRecords[0].RData, ips[0])
}
dnsServer.Stop()
ctx, cancel := context.WithTimeout(context.TODO(), time.Second*1)
defer cancel()
_, err = resolver.LookupHost(ctx, zoneRecords[0].Name)
if err == nil {
t.Fatalf("we should encounter an error when querying a stopped server")
}
})
}
}
func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
hostManager := &mockHostConfigurator{}
server := DefaultServer{
dnsMux: dns.DefaultServeMux,
localResolver: &localResolver{
registeredMap: make(registrationMap),
},
hostManager: hostManager,
currentConfig: hostDNSConfig{
domains: []domainConfig{
{false, "domain0", false},
{false, "domain1", false},
{false, "domain2", false},
},
},
}
var domainsUpdate string
hostManager.applyDNSConfigFunc = func(config hostDNSConfig) error {
domains := []string{}
for _, item := range config.domains {
if item.disabled {
continue
}
domains = append(domains, item.domain)
}
domainsUpdate = strings.Join(domains, ",")
return nil
}
deactivate, reactivate := server.upstreamCallbacks(&nbdns.NameServerGroup{
Domains: []string{"domain1"},
NameServers: []nbdns.NameServer{
{IP: netip.MustParseAddr("8.8.0.0"), NSType: nbdns.UDPNameServerType, Port: 53},
},
}, nil)
deactivate()
expected := "domain0,domain2"
domains := []string{}
for _, item := range server.currentConfig.domains {
if item.disabled {
continue
}
domains = append(domains, item.domain)
}
got := strings.Join(domains, ",")
if expected != got {
t.Errorf("expected domains list: %q, got %q", expected, got)
}
reactivate()
expected = "domain0,domain1,domain2"
domains = []string{}
for _, item := range server.currentConfig.domains {
if item.disabled {
continue
}
domains = append(domains, item.domain)
}
got = strings.Join(domains, ",")
if expected != got {
t.Errorf("expected domains list: %q, got %q", expected, domainsUpdate)
}
}
func getDefaultServerWithNoHostManager(t *testing.T, addrPort string) *DefaultServer {
mux := dns.NewServeMux()
var parsedAddrPort *netip.AddrPort
if addrPort != "" {
parsed, err := netip.ParseAddrPort(addrPort)
if err != nil {
t.Fatal(err)
}
parsedAddrPort = &parsed
}
dnsServer := &dns.Server{
Net: "udp",
Handler: mux,
UDPSize: 65535,
}
ctx, cancel := context.WithCancel(context.TODO())
return &DefaultServer{
ctx: ctx,
ctxCancel: cancel,
server: dnsServer,
dnsMux: mux,
dnsMuxMap: make(registrationMap),
localResolver: &localResolver{
registeredMap: make(registrationMap),
},
customAddress: parsedAddrPort,
}
}

View File

@@ -1,210 +0,0 @@
package dns
import (
"context"
"fmt"
"net"
"net/netip"
"time"
"github.com/godbus/dbus/v5"
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
const (
systemdDbusManagerInterface = "org.freedesktop.resolve1.Manager"
systemdResolvedDest = "org.freedesktop.resolve1"
systemdDbusObjectNode = "/org/freedesktop/resolve1"
systemdDbusGetLinkMethod = systemdDbusManagerInterface + ".GetLink"
systemdDbusFlushCachesMethod = systemdDbusManagerInterface + ".FlushCaches"
systemdDbusResolvConfModeProperty = systemdDbusManagerInterface + ".ResolvConfMode"
systemdDbusLinkInterface = "org.freedesktop.resolve1.Link"
systemdDbusRevertMethodSuffix = systemdDbusLinkInterface + ".Revert"
systemdDbusSetDNSMethodSuffix = systemdDbusLinkInterface + ".SetDNS"
systemdDbusSetDefaultRouteMethodSuffix = systemdDbusLinkInterface + ".SetDefaultRoute"
systemdDbusSetDomainsMethodSuffix = systemdDbusLinkInterface + ".SetDomains"
systemdDbusResolvConfModeForeign = "foreign"
)
type systemdDbusConfigurator struct {
dbusLinkObject dbus.ObjectPath
routingAll bool
}
// the types below are based on dbus specification, each field is mapped to a dbus type
// see https://dbus.freedesktop.org/doc/dbus-specification.html#basic-types for more details on dbus types
// see https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html on resolve1 input types
// systemdDbusDNSInput maps to a (iay) dbus input for SetDNS method
type systemdDbusDNSInput struct {
Family int32
Address []byte
}
// systemdDbusLinkDomainsInput maps to a (sb) dbus input for SetDomains method
type systemdDbusLinkDomainsInput struct {
Domain string
MatchOnly bool
}
func newSystemdDbusConfigurator(wgInterface *iface.WGIface) (hostManager, error) {
iface, err := net.InterfaceByName(wgInterface.Name())
if err != nil {
return nil, err
}
obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode)
if err != nil {
return nil, err
}
defer closeConn()
var s string
err = obj.Call(systemdDbusGetLinkMethod, dbusDefaultFlag, iface.Index).Store(&s)
if err != nil {
return nil, err
}
log.Debugf("got dbus Link interface: %s from net interface %s and index %d", s, iface.Name, iface.Index)
return &systemdDbusConfigurator{
dbusLinkObject: dbus.ObjectPath(s),
}, nil
}
func (s *systemdDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
parsedIP, err := netip.ParseAddr(config.serverIP)
if err != nil {
return fmt.Errorf("unable to parse ip address, error: %s", err)
}
ipAs4 := parsedIP.As4()
defaultLinkInput := systemdDbusDNSInput{
Family: unix.AF_INET,
Address: ipAs4[:],
}
err = s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput})
if err != nil {
return fmt.Errorf("setting the interface DNS server %s:%d failed with error: %s", config.serverIP, config.serverPort, err)
}
var (
searchDomains []string
matchDomains []string
domainsInput []systemdDbusLinkDomainsInput
)
for _, dConf := range config.domains {
if dConf.disabled {
continue
}
domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{
Domain: dns.Fqdn(dConf.domain),
MatchOnly: dConf.matchOnly,
})
if dConf.matchOnly {
matchDomains = append(matchDomains, dConf.domain)
continue
}
searchDomains = append(searchDomains, dConf.domain)
}
if config.routeAll {
log.Infof("configured %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
err = s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, true)
if err != nil {
return fmt.Errorf("setting link as default dns router, failed with error: %s", err)
}
domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{
Domain: nbdns.RootZone,
MatchOnly: true,
})
s.routingAll = true
} else if s.routingAll {
log.Infof("removing %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
}
log.Infof("adding %d search domains and %d match domains. Search list: %s , Match list: %s", len(searchDomains), len(matchDomains), searchDomains, matchDomains)
err = s.setDomainsForInterface(domainsInput)
if err != nil {
log.Error(err)
}
return nil
}
func (s *systemdDbusConfigurator) setDomainsForInterface(domainsInput []systemdDbusLinkDomainsInput) error {
err := s.callLinkMethod(systemdDbusSetDomainsMethodSuffix, domainsInput)
if err != nil {
return fmt.Errorf("setting domains configuration failed with error: %s", err)
}
return s.flushCaches()
}
func (s *systemdDbusConfigurator) restoreHostDNS() error {
log.Infof("reverting link settings and flushing cache")
if !isDbusListenerRunning(systemdResolvedDest, s.dbusLinkObject) {
return nil
}
err := s.callLinkMethod(systemdDbusRevertMethodSuffix, nil)
if err != nil {
return fmt.Errorf("unable to revert link configuration, got error: %s", err)
}
return s.flushCaches()
}
func (s *systemdDbusConfigurator) flushCaches() error {
obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode)
if err != nil {
return fmt.Errorf("got error while attempting to retrieve the object %s, err: %s", systemdDbusObjectNode, err)
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
err = obj.CallWithContext(ctx, systemdDbusFlushCachesMethod, dbusDefaultFlag).Store()
if err != nil {
return fmt.Errorf("got error while calling the FlushCaches method with context, err: %s", err)
}
return nil
}
func (s *systemdDbusConfigurator) callLinkMethod(method string, value any) error {
obj, closeConn, err := getDbusObject(systemdResolvedDest, s.dbusLinkObject)
if err != nil {
return fmt.Errorf("got error while attempting to retrieve the object, err: %s", err)
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
if value != nil {
err = obj.CallWithContext(ctx, method, dbusDefaultFlag, value).Store()
} else {
err = obj.CallWithContext(ctx, method, dbusDefaultFlag).Store()
}
if err != nil {
return fmt.Errorf("got error while calling command with context, err: %s", err)
}
return nil
}
func getSystemdDbusProperty(property string, store any) error {
obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode)
if err != nil {
return fmt.Errorf("got error while attempting to retrieve the systemd dns manager object, error: %s", err)
}
defer closeConn()
v, e := obj.GetProperty(property)
if e != nil {
return fmt.Errorf("got an error getting property %s: %v", property, e)
}
return v.Store(store)
}

View File

@@ -1,143 +0,0 @@
package dns
import (
"context"
"errors"
"net"
"sync"
"sync/atomic"
"time"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
)
const (
failsTillDeact = int32(3)
reactivatePeriod = time.Minute
upstreamTimeout = 15 * time.Second
)
type upstreamResolver struct {
ctx context.Context
upstreamClient *dns.Client
upstreamServers []string
disabled bool
failsCount atomic.Int32
failsTillDeact int32
mutex sync.Mutex
reactivatePeriod time.Duration
upstreamTimeout time.Duration
deactivate func()
reactivate func()
}
func newUpstreamResolver(ctx context.Context) *upstreamResolver {
return &upstreamResolver{
ctx: ctx,
upstreamClient: &dns.Client{},
upstreamTimeout: upstreamTimeout,
reactivatePeriod: reactivatePeriod,
failsTillDeact: failsTillDeact,
}
}
// ServeDNS handles a DNS request
func (u *upstreamResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
defer u.checkUpstreamFails()
log.WithField("question", r.Question[0]).Trace("received an upstream question")
select {
case <-u.ctx.Done():
return
default:
}
for _, upstream := range u.upstreamServers {
ctx, cancel := context.WithTimeout(u.ctx, u.upstreamTimeout)
rm, t, err := u.upstreamClient.ExchangeContext(ctx, r, upstream)
cancel()
if err != nil {
if err == context.DeadlineExceeded || isTimeout(err) {
log.WithError(err).WithField("upstream", upstream).
Warn("got an error while connecting to upstream")
continue
}
u.failsCount.Add(1)
log.WithError(err).WithField("upstream", upstream).
Error("got an error while querying the upstream")
return
}
log.Tracef("took %s to query the upstream %s", t, upstream)
err = w.WriteMsg(rm)
if err != nil {
log.WithError(err).Error("got an error while writing the upstream resolver response")
}
// count the fails only if they happen sequentially
u.failsCount.Store(0)
return
}
u.failsCount.Add(1)
log.Error("all queries to the upstream nameservers failed with timeout")
}
// checkUpstreamFails counts fails and disables or enables upstream resolving
//
// If fails count is greater that failsTillDeact, upstream resolving
// will be disabled for reactivatePeriod, after that time period fails counter
// will be reset and upstream will be reactivated.
func (u *upstreamResolver) checkUpstreamFails() {
u.mutex.Lock()
defer u.mutex.Unlock()
if u.failsCount.Load() < u.failsTillDeact || u.disabled {
return
}
select {
case <-u.ctx.Done():
return
default:
log.Warnf("upstream resolving is disabled for %v", reactivatePeriod)
u.deactivate()
u.disabled = true
go u.waitUntilReactivation()
}
}
// waitUntilReactivation reset fails counter and activates upstream resolving
func (u *upstreamResolver) waitUntilReactivation() {
timer := time.NewTimer(u.reactivatePeriod)
defer func() {
if !timer.Stop() {
<-timer.C
}
}()
select {
case <-u.ctx.Done():
return
case <-timer.C:
log.Info("upstream resolving is reactivated")
u.failsCount.Store(0)
u.reactivate()
u.disabled = false
}
}
// isTimeout returns true if the given error is a network timeout error.
//
// Copied from k8s.io/apimachinery/pkg/util/net.IsTimeout
func isTimeout(err error) bool {
var neterr net.Error
if errors.As(err, &neterr) {
return neterr != nil && neterr.Timeout()
}
return false
}

View File

@@ -1,156 +0,0 @@
package dns
import (
"context"
"github.com/miekg/dns"
"strings"
"testing"
"time"
)
func TestUpstreamResolver_ServeDNS(t *testing.T) {
testCases := []struct {
name string
inputMSG *dns.Msg
responseShouldBeNil bool
InputServers []string
timeout time.Duration
cancelCTX bool
expectedAnswer string
}{
{
name: "Should Resolve A Record",
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
InputServers: []string{"8.8.8.8:53", "8.8.4.4:53"},
timeout: upstreamTimeout,
expectedAnswer: "1.1.1.1",
},
{
name: "Should Resolve If First Upstream Times Out",
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
InputServers: []string{"8.0.0.0:53", "8.8.4.4:53"},
timeout: 2 * time.Second,
expectedAnswer: "1.1.1.1",
},
{
name: "Should Not Resolve If Can't Connect To Both Servers",
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
InputServers: []string{"8.0.0.0:53", "8.0.0.1:53"},
timeout: 200 * time.Millisecond,
responseShouldBeNil: true,
},
{
name: "Should Not Resolve If Parent Context Is Canceled",
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
InputServers: []string{"8.0.0.0:53", "8.8.4.4:53"},
cancelCTX: true,
timeout: upstreamTimeout,
responseShouldBeNil: true,
},
//{
// name: "Should Resolve CNAME Record",
// inputMSG: new(dns.Msg).SetQuestion("one.one.one.one", dns.TypeCNAME),
//},
//{
// name: "Should Not Write When Not Found A Record",
// inputMSG: new(dns.Msg).SetQuestion("not.found.com", dns.TypeA),
// responseShouldBeNil: true,
//},
}
// should resolve if first upstream times out
// should not write when both fails
// should not resolve if parent context is canceled
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.TODO())
resolver := newUpstreamResolver(ctx)
resolver.upstreamServers = testCase.InputServers
resolver.upstreamTimeout = testCase.timeout
if testCase.cancelCTX {
cancel()
} else {
defer cancel()
}
var responseMSG *dns.Msg
responseWriter := &mockResponseWriter{
WriteMsgFunc: func(m *dns.Msg) error {
responseMSG = m
return nil
},
}
resolver.ServeDNS(responseWriter, testCase.inputMSG)
if responseMSG == nil {
if testCase.responseShouldBeNil {
return
}
t.Fatalf("should write a response message")
}
foundAnswer := false
for _, answer := range responseMSG.Answer {
if strings.Contains(answer.String(), testCase.expectedAnswer) {
foundAnswer = true
break
}
}
if !foundAnswer {
t.Errorf("couldn't find the required answer, %s, in the dns response", testCase.expectedAnswer)
}
})
}
}
func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
resolver := newUpstreamResolver(context.TODO())
resolver.upstreamServers = []string{"0.0.0.0:-1"}
resolver.failsTillDeact = 0
resolver.reactivatePeriod = time.Microsecond * 100
responseWriter := &mockResponseWriter{
WriteMsgFunc: func(m *dns.Msg) error { return nil },
}
failed := false
resolver.deactivate = func() {
failed = true
}
reactivated := false
resolver.reactivate = func() {
reactivated = true
}
resolver.ServeDNS(responseWriter, new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA))
if !failed {
t.Errorf("expected that resolving was deactivated")
return
}
if !resolver.disabled {
t.Errorf("resolver should be disabled")
return
}
time.Sleep(time.Millisecond * 200)
if !reactivated {
t.Errorf("expected that resolving was reactivated")
return
}
if resolver.failsCount.Load() != 0 {
t.Errorf("fails count after reactivation should be 0")
return
}
if resolver.disabled {
t.Errorf("should be enabled")
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
//go:build !android
package internal
import (
"github.com/netbirdio/netbird/client/internal/stdnet"
)
func (e *Engine) newStdNet() (*stdnet.Net, error) {
return stdnet.NewNet(e.config.IFaceBlackList)
}

View File

@@ -1,7 +0,0 @@
package internal
import "github.com/netbirdio/netbird/client/internal/stdnet"
func (e *Engine) newStdNet() (*stdnet.Net, error) {
return stdnet.NewNetWithDiscover(e.mobileDep.IFaceDiscover, e.config.IFaceBlackList)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,161 +0,0 @@
package internal
import (
"context"
"net/url"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"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"
)
// IsLoginRequired check that the server is support SSO or not
func IsLoginRequired(ctx context.Context, privateKey string, mgmURL *url.URL, sshKey string) (bool, error) {
mgmClient, err := getMgmClient(ctx, privateKey, mgmURL)
if err != nil {
return false, err
}
defer func() {
err = mgmClient.Close()
if err != nil {
cStatus, ok := status.FromError(err)
if !ok || ok && cStatus.Code() != codes.Canceled {
log.Warnf("failed to close the Management service client, err: %v", err)
}
}
}()
log.Debugf("connected to the Management service %s", mgmURL.String())
pubSSHKey, err := ssh.GeneratePublicKey([]byte(sshKey))
if err != nil {
return false, err
}
_, err = doMgmLogin(ctx, mgmClient, pubSSHKey)
if isLoginNeeded(err) {
return true, nil
}
return false, err
}
// Login or register the client
func Login(ctx context.Context, config *Config, setupKey string, jwtToken string) error {
mgmClient, err := getMgmClient(ctx, config.PrivateKey, config.ManagementURL)
if err != nil {
return err
}
defer func() {
err = mgmClient.Close()
if err != nil {
cStatus, ok := status.FromError(err)
if !ok || ok && cStatus.Code() != codes.Canceled {
log.Warnf("failed to close the Management service client, err: %v", err)
}
}
}()
log.Debugf("connected to the Management service %s", config.ManagementURL.String())
pubSSHKey, err := ssh.GeneratePublicKey([]byte(config.SSHKey))
if err != nil {
return err
}
serverKey, err := doMgmLogin(ctx, mgmClient, pubSSHKey)
if isRegistrationNeeded(err) {
log.Debugf("peer registration required")
_, err = registerPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey)
return err
}
return err
}
func getMgmClient(ctx context.Context, privateKey string, mgmURL *url.URL) (*mgm.GrpcClient, error) {
// validate our peer's Wireguard PRIVATE key
myPrivateKey, err := wgtypes.ParseKey(privateKey)
if err != nil {
log.Errorf("failed parsing Wireguard key %s: [%s]", privateKey, err.Error())
return nil, err
}
var mgmTlsEnabled bool
if mgmURL.Scheme == "https" {
mgmTlsEnabled = true
}
log.Debugf("connecting to the Management service %s", mgmURL.String())
mgmClient, err := mgm.NewClient(ctx, mgmURL.Host, myPrivateKey, mgmTlsEnabled)
if err != nil {
log.Errorf("failed connecting to the Management service %s %v", mgmURL.String(), err)
return nil, err
}
return mgmClient, err
}
func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte) (*wgtypes.Key, error) {
serverKey, err := mgmClient.GetServerPublicKey()
if err != nil {
log.Errorf("failed while getting Management Service public key: %v", err)
return nil, err
}
sysInfo := system.GetInfo(ctx)
_, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey)
return serverKey, err
}
// 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, 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)
}
log.Debugf("sending peer registration request to Management Service")
info := system.GetInfo(ctx)
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
}
log.Infof("peer has been successfully registered on Management Service")
return loginResp, nil
}
func isLoginNeeded(err error) bool {
if err == nil {
return false
}
s, ok := status.FromError(err)
if !ok {
return false
}
if s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied {
return true
}
return false
}
func isRegistrationNeeded(err error) bool {
if err == nil {
return false
}
s, ok := status.FromError(err)
if !ok {
return false
}
if s.Code() == codes.PermissionDenied {
return true
}
return false
}

View File

@@ -1,13 +0,0 @@
package internal
import (
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/iface"
)
// MobileDependency collect all dependencies for mobile platform
type MobileDependency struct {
TunAdapter iface.TunAdapter
IFaceDiscover stdnet.ExternalIFaceDiscover
Routes []string
}

View File

@@ -1,29 +0,0 @@
package internal
import (
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/iface"
mgm "github.com/netbirdio/netbird/management/client"
)
func newMobileDependency(tunAdapter iface.TunAdapter, ifaceDiscover stdnet.ExternalIFaceDiscover, mgmClient *mgm.GrpcClient) (MobileDependency, error) {
md := MobileDependency{
TunAdapter: tunAdapter,
IFaceDiscover: ifaceDiscover,
}
err := md.readMap(mgmClient)
return md, err
}
func (d *MobileDependency) readMap(mgmClient *mgm.GrpcClient) error {
routes, err := mgmClient.GetRoutes()
if err != nil {
return err
}
d.Routes = make([]string, len(routes))
for i, r := range routes {
d.Routes[i] = r.GetNetwork()
}
return nil
}

View File

@@ -1,13 +0,0 @@
//go:build !android
package internal
import (
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/iface"
mgm "github.com/netbirdio/netbird/management/client"
)
func newMobileDependency(tunAdapter iface.TunAdapter, ifaceDiscover stdnet.ExternalIFaceDiscover, mgmClient *mgm.GrpcClient) (MobileDependency, error) {
return MobileDependency{}, nil
}

View File

@@ -1,281 +0,0 @@
package internal
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"reflect"
"strings"
"time"
)
// OAuthClient is a OAuth client interface for various idp providers
type OAuthClient interface {
RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error)
GetClientID(ctx context.Context) string
}
// HTTPClient http client interface for API calls
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// DeviceAuthInfo holds information for the OAuth device login flow
type DeviceAuthInfo struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
// HostedGrantType grant type for device flow on Hosted
const (
HostedGrantType = "urn:ietf:params:oauth:grant-type:device_code"
HostedRefreshGrant = "refresh_token"
)
// Hosted client
type Hosted struct {
providerConfig ProviderConfig
HTTPClient HTTPClient
}
// RequestDeviceCodePayload used for request device code payload for auth0
type RequestDeviceCodePayload struct {
Audience string `json:"audience"`
ClientID string `json:"client_id"`
Scope string `json:"scope"`
}
// TokenRequestPayload used for requesting the auth0 token
type TokenRequestPayload struct {
GrantType string `json:"grant_type"`
DeviceCode string `json:"device_code,omitempty"`
ClientID string `json:"client_id"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// TokenRequestResponse used for parsing Hosted token's response
type TokenRequestResponse struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
TokenInfo
}
// Claims used when validating the access token
type Claims struct {
Audience interface{} `json:"aud"`
}
// TokenInfo holds information of issued access token
type TokenInfo struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
UseIDToken bool `json:"-"`
}
// GetTokenToUse returns either the access or id token based on UseIDToken field
func (t TokenInfo) GetTokenToUse() string {
if t.UseIDToken {
return t.IDToken
}
return t.AccessToken
}
// NewHostedDeviceFlow returns an Hosted OAuth client
func NewHostedDeviceFlow(config ProviderConfig) *Hosted {
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
httpTransport.MaxIdleConns = 5
httpClient := &http.Client{
Timeout: 10 * time.Second,
Transport: httpTransport,
}
return &Hosted{
providerConfig: config,
HTTPClient: httpClient,
}
}
// GetClientID returns the provider client id
func (h *Hosted) GetClientID(ctx context.Context) string {
return h.providerConfig.ClientID
}
// RequestDeviceCode requests a device code login flow information from Hosted
func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error) {
form := url.Values{}
form.Add("client_id", h.providerConfig.ClientID)
form.Add("audience", h.providerConfig.Audience)
form.Add("scope", h.providerConfig.Scope)
req, err := http.NewRequest("POST", h.providerConfig.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/x-www-form-urlencoded")
res, err := h.HTTPClient.Do(req)
if err != nil {
return DeviceAuthInfo{}, fmt.Errorf("doing request failed with error: %v", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return DeviceAuthInfo{}, fmt.Errorf("reading body failed with error: %v", err)
}
if res.StatusCode != 200 {
return DeviceAuthInfo{}, fmt.Errorf("request device code returned status %d error: %s", res.StatusCode, string(body))
}
deviceCode := DeviceAuthInfo{}
err = json.Unmarshal(body, &deviceCode)
if err != nil {
return DeviceAuthInfo{}, fmt.Errorf("unmarshaling response failed with error: %v", err)
}
return deviceCode, err
}
func (h *Hosted) requestToken(info DeviceAuthInfo) (TokenRequestResponse, error) {
form := url.Values{}
form.Add("client_id", h.providerConfig.ClientID)
form.Add("grant_type", HostedGrantType)
form.Add("device_code", info.DeviceCode)
req, err := http.NewRequest("POST", h.providerConfig.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) {
interval := time.Duration(info.Interval) * time.Second
ticker := time.NewTicker(interval)
for {
select {
case <-ctx.Done():
return TokenInfo{}, ctx.Err()
case <-ticker.C:
tokenResponse, err := h.requestToken(info)
if err != nil {
return TokenInfo{}, fmt.Errorf("parsing token response failed with error: %v", err)
}
if tokenResponse.Error != "" {
if tokenResponse.Error == "authorization_pending" {
continue
} else if tokenResponse.Error == "slow_down" {
interval = interval + (3 * time.Second)
ticker.Reset(interval)
continue
}
return TokenInfo{}, fmt.Errorf(tokenResponse.ErrorDescription)
}
tokenInfo := TokenInfo{
AccessToken: tokenResponse.AccessToken,
TokenType: tokenResponse.TokenType,
RefreshToken: tokenResponse.RefreshToken,
IDToken: tokenResponse.IDToken,
ExpiresIn: tokenResponse.ExpiresIn,
UseIDToken: h.providerConfig.UseIDToken,
}
err = isValidAccessToken(tokenInfo.GetTokenToUse(), h.providerConfig.Audience)
if err != nil {
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
}
return tokenInfo, err
}
}
}
// isValidAccessToken is a simple validation of the access token
func isValidAccessToken(token string, audience string) error {
if token == "" {
return fmt.Errorf("token received is empty")
}
encodedClaims := strings.Split(token, ".")[1]
claimsString, err := base64.RawURLEncoding.DecodeString(encodedClaims)
if err != nil {
return err
}
claims := Claims{}
err = json.Unmarshal(claimsString, &claims)
if err != nil {
return err
}
if claims.Audience == nil {
return fmt.Errorf("required token field audience is absent")
}
// Audience claim of JWT can be a string or an array of strings
typ := reflect.TypeOf(claims.Audience)
switch typ.Kind() {
case reflect.String:
if claims.Audience == audience {
return nil
}
case reflect.Slice:
for _, aud := range claims.Audience.([]interface{}) {
if audience == aud {
return nil
}
}
}
return fmt.Errorf("invalid JWT token audience field")
}

View File

@@ -1,305 +0,0 @@
package internal
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt"
"github.com/stretchr/testify/require"
)
type mockHTTPClient struct {
code int
resBody string
reqBody string
MaxReqs int
count int
countResBody string
err error
}
func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
body, err := io.ReadAll(req.Body)
if err == nil {
c.reqBody = string(body)
}
if c.MaxReqs > c.count {
c.count++
return &http.Response{
StatusCode: c.code,
Body: io.NopCloser(strings.NewReader(c.countResBody)),
}, c.err
}
return &http.Response{
StatusCode: c.code,
Body: io.NopCloser(strings.NewReader(c.resBody)),
}, c.err
}
func TestHosted_RequestDeviceCode(t *testing.T) {
type test struct {
name string
inputResBody string
inputReqCode int
inputReqError error
testingErrFunc require.ErrorAssertionFunc
expectedErrorMSG string
testingFunc require.ComparisonAssertionFunc
expectedOut DeviceAuthInfo
expectedMSG string
expectPayload string
}
expectedAudience := "ok"
expectedClientID := "bla"
expectedScope := "openid"
form := url.Values{}
form.Add("audience", expectedAudience)
form.Add("client_id", expectedClientID)
form.Add("scope", expectedScope)
expectPayload := form.Encode()
testCase1 := test{
name: "Payload Is Valid",
expectPayload: expectPayload,
inputReqCode: 200,
testingErrFunc: require.Error,
testingFunc: require.EqualValues,
}
testCase2 := test{
name: "Exit On Network Error",
inputReqError: fmt.Errorf("error"),
testingErrFunc: require.Error,
expectedErrorMSG: "should return error",
testingFunc: require.EqualValues,
expectPayload: expectPayload,
}
testCase3 := test{
name: "Exit On Exit Code",
inputReqCode: 400,
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: expectPayload,
inputReqCode: 200,
testingErrFunc: require.NoError,
testingFunc: require.EqualValues,
expectedOut: testCase4Out,
expectedMSG: "out should match",
}
for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4} {
t.Run(testCase.name, func(t *testing.T) {
httpClient := mockHTTPClient{
resBody: testCase.inputResBody,
code: testCase.inputReqCode,
err: testCase.inputReqError,
}
hosted := Hosted{
providerConfig: ProviderConfig{
Audience: expectedAudience,
ClientID: expectedClientID,
Scope: expectedScope,
TokenEndpoint: "test.hosted.com/token",
DeviceAuthEndpoint: "test.hosted.com/device/auth",
UseIDToken: false,
},
HTTPClient: &httpClient,
}
authInfo, err := hosted.RequestDeviceCode(context.TODO())
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
require.EqualValues(t, expectPayload, httpClient.reqBody, "payload should match")
testCase.testingFunc(t, testCase.expectedOut, authInfo, testCase.expectedMSG)
})
}
}
func TestHosted_WaitToken(t *testing.T) {
type test struct {
name string
inputResBody string
inputReqCode int
inputReqError error
inputMaxReqs int
inputCountResBody string
inputTimeout time.Duration
inputInfo DeviceAuthInfo
inputAudience string
testingErrFunc require.ErrorAssertionFunc
expectedErrorMSG string
testingFunc require.ComparisonAssertionFunc
expectedOut TokenInfo
expectedMSG string
expectPayload string
}
defaultInfo := DeviceAuthInfo{
DeviceCode: "test",
ExpiresIn: 10,
Interval: 1,
}
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",
inputInfo: defaultInfo,
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
inputReqCode: 200,
testingErrFunc: require.Error,
testingFunc: require.EqualValues,
expectPayload: tokenReqPayload,
}
testCase2 := test{
name: "Exit On Network Error",
inputInfo: defaultInfo,
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
expectPayload: tokenReqPayload,
inputReqError: fmt.Errorf("error"),
testingErrFunc: require.Error,
expectedErrorMSG: "should return error",
testingFunc: require.EqualValues,
}
testCase3 := test{
name: "Exit On 4XX When Not Pending",
inputInfo: defaultInfo,
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
inputReqCode: 400,
expectPayload: tokenReqPayload,
testingErrFunc: require.Error,
expectedErrorMSG: "should return error",
testingFunc: require.EqualValues,
}
testCase4 := test{
name: "Exit On Exit Code 5XX",
inputInfo: defaultInfo,
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
inputReqCode: 500,
expectPayload: tokenReqPayload,
testingErrFunc: require.Error,
expectedErrorMSG: "should return error",
testingFunc: require.EqualValues,
}
testCase5 := test{
name: "Exit On Content Timeout",
inputInfo: defaultInfo,
inputTimeout: 0 * time.Second,
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)
testCase6 := test{
name: "Exit On Invalid Audience",
inputInfo: defaultInfo,
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\"}", tokenString),
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
inputReqCode: 200,
inputAudience: "super test",
testingErrFunc: require.Error,
testingFunc: require.EqualValues,
expectPayload: tokenReqPayload,
}
testCase7 := test{
name: "Received Token Info",
inputInfo: defaultInfo,
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\"}", tokenString),
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
inputReqCode: 200,
inputAudience: audience,
testingErrFunc: require.NoError,
testingFunc: require.EqualValues,
expectPayload: tokenReqPayload,
expectedOut: TokenInfo{AccessToken: tokenString},
}
testCase8 := test{
name: "Received Token Info after Multiple tries",
inputInfo: defaultInfo,
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\"}", tokenString),
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
inputMaxReqs: 2,
inputCountResBody: "{\"error\":\"authorization_pending\"}",
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, testCase6, testCase7, testCase8} {
t.Run(testCase.name, func(t *testing.T) {
httpClient := mockHTTPClient{
resBody: testCase.inputResBody,
code: testCase.inputReqCode,
err: testCase.inputReqError,
MaxReqs: testCase.inputMaxReqs,
countResBody: testCase.inputCountResBody,
}
hosted := Hosted{
providerConfig: ProviderConfig{
Audience: testCase.inputAudience,
ClientID: clientID,
TokenEndpoint: "test.hosted.com/token",
DeviceAuthEndpoint: "test.hosted.com/device/auth",
Scope: "openid",
UseIDToken: false,
},
HTTPClient: &httpClient}
ctx, cancel := context.WithTimeout(context.TODO(), testCase.inputTimeout)
defer cancel()
tokenInfo, err := hosted.WaitToken(ctx, testCase.inputInfo)
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
require.EqualValues(t, testCase.expectPayload, httpClient.reqBody, "payload should match")
testCase.testingFunc(t, testCase.expectedOut, tokenInfo, testCase.expectedMSG)
require.GreaterOrEqualf(t, testCase.inputMaxReqs, httpClient.count, "should run %d times", testCase.inputMaxReqs)
})
}
}

View File

@@ -1,731 +0,0 @@
package peer
import (
"context"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/pion/ice/v2"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/proxy"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/iface"
signal "github.com/netbirdio/netbird/signal/client"
sProto "github.com/netbirdio/netbird/signal/proto"
"github.com/netbirdio/netbird/version"
)
// ConnConfig is a peer Connection configuration
type ConnConfig struct {
// Key is a public key of a remote peer
Key string
// LocalKey is a public key of a local peer
LocalKey string
// StunTurn is a list of STUN and TURN URLs
StunTurn []*ice.URL
// InterfaceBlackList is a list of machine interfaces that should be filtered out by ICE Candidate gathering
// (e.g. if eth0 is in the list, host candidate of this interface won't be used)
InterfaceBlackList []string
DisableIPv6Discovery bool
Timeout time.Duration
ProxyConfig proxy.Config
UDPMux ice.UDPMux
UDPMuxSrflx ice.UniversalUDPMux
LocalWgPort int
NATExternalIPs []string
// UsesBind indicates whether the WireGuard interface is userspace and uses bind.ICEBind
UserspaceBind bool
}
// OfferAnswer represents a session establishment offer or answer
type OfferAnswer struct {
IceCredentials IceCredentials
// WgListenPort is a remote WireGuard listen port.
// This field is used when establishing a direct WireGuard connection without any proxy.
// We can set the remote peer's endpoint with this port.
WgListenPort int
// Version of NetBird Agent
Version string
}
// IceCredentials ICE protocol credentials struct
type IceCredentials struct {
UFrag string
Pwd string
}
type Conn struct {
config ConnConfig
mu sync.Mutex
// signalCandidate is a handler function to signal remote peer about local connection candidate
signalCandidate func(candidate ice.Candidate) error
// signalOffer is a handler function to signal remote peer our connection offer (credentials)
signalOffer func(OfferAnswer) error
signalAnswer func(OfferAnswer) error
sendSignalMessage func(message *sProto.Message) error
// remoteOffersCh is a channel used to wait for remote credentials to proceed with the connection
remoteOffersCh chan OfferAnswer
// remoteAnswerCh is a channel used to wait for remote credentials answer (confirmation of our offer) to proceed with the connection
remoteAnswerCh chan OfferAnswer
closeCh chan struct{}
ctx context.Context
notifyDisconnected context.CancelFunc
agent *ice.Agent
status ConnStatus
statusRecorder *Status
proxy proxy.Proxy
remoteModeCh chan ModeMessage
meta meta
adapter iface.TunAdapter
iFaceDiscover stdnet.ExternalIFaceDiscover
}
// meta holds meta information about a connection
type meta struct {
protoSupport signal.FeaturesSupport
}
// ModeMessage represents a connection mode chosen by the peer
type ModeMessage struct {
// Direct indicates that it decided to use a direct connection
Direct bool
}
// GetConf returns the connection config
func (conn *Conn) GetConf() ConnConfig {
return conn.config
}
// UpdateConf updates the connection config
func (conn *Conn) UpdateConf(conf ConnConfig) {
conn.config = conf
}
// NewConn creates a new not opened Conn to the remote peer.
// To establish a connection run Conn.Open
func NewConn(config ConnConfig, statusRecorder *Status, adapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover) (*Conn, error) {
return &Conn{
config: config,
mu: sync.Mutex{},
status: StatusDisconnected,
closeCh: make(chan struct{}),
remoteOffersCh: make(chan OfferAnswer),
remoteAnswerCh: make(chan OfferAnswer),
statusRecorder: statusRecorder,
remoteModeCh: make(chan ModeMessage, 1),
adapter: adapter,
iFaceDiscover: iFaceDiscover,
}, nil
}
func (conn *Conn) reCreateAgent() error {
conn.mu.Lock()
defer conn.mu.Unlock()
failedTimeout := 6 * time.Second
var err error
transportNet, err := conn.newStdNet()
if err != nil {
log.Errorf("failed to create pion's stdnet: %s", err)
}
agentConfig := &ice.AgentConfig{
MulticastDNSMode: ice.MulticastDNSModeDisabled,
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6},
Urls: conn.config.StunTurn,
CandidateTypes: []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive, ice.CandidateTypeRelay},
FailedTimeout: &failedTimeout,
InterfaceFilter: stdnet.InterfaceFilter(conn.config.InterfaceBlackList),
UDPMux: conn.config.UDPMux,
UDPMuxSrflx: conn.config.UDPMuxSrflx,
NAT1To1IPs: conn.config.NATExternalIPs,
Net: transportNet,
}
if conn.config.DisableIPv6Discovery {
agentConfig.NetworkTypes = []ice.NetworkType{ice.NetworkTypeUDP4}
}
conn.agent, err = ice.NewAgent(agentConfig)
if err != nil {
return err
}
err = conn.agent.OnCandidate(conn.onICECandidate)
if err != nil {
return err
}
err = conn.agent.OnConnectionStateChange(conn.onICEConnectionStateChange)
if err != nil {
return err
}
err = conn.agent.OnSelectedCandidatePairChange(conn.onICESelectedCandidatePair)
if err != nil {
return err
}
return nil
}
// Open opens connection to the remote peer starting ICE candidate gathering process.
// Blocks until connection has been closed or connection timeout.
// ConnStatus will be set accordingly
func (conn *Conn) Open() error {
log.Debugf("trying to connect to peer %s", conn.config.Key)
peerState := State{PubKey: conn.config.Key}
peerState.IP = strings.Split(conn.config.ProxyConfig.AllowedIps, "/")[0]
peerState.ConnStatusUpdate = time.Now()
peerState.ConnStatus = conn.status
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 {
log.Warnf("error while cleaning up peer connection %s: %v", conn.config.Key, err)
return
}
}()
err = conn.reCreateAgent()
if err != nil {
return err
}
err = conn.sendOffer()
if err != nil {
return err
}
log.Debugf("connection offer sent to peer %s, waiting for the confirmation", conn.config.Key)
// Only continue once we got a connection confirmation from the remote peer.
// The connection timeout could have happened before a confirmation received from the remote.
// The connection could have also been closed externally (e.g. when we received an update from the management that peer shouldn't be connected)
var remoteOfferAnswer OfferAnswer
select {
case remoteOfferAnswer = <-conn.remoteOffersCh:
// received confirmation from the remote peer -> ready to proceed
err = conn.sendAnswer()
if err != nil {
return err
}
case remoteOfferAnswer = <-conn.remoteAnswerCh:
case <-time.After(conn.config.Timeout):
return NewConnectionTimeoutError(conn.config.Key, conn.config.Timeout)
case <-conn.closeCh:
// closed externally
return NewConnectionClosedError(conn.config.Key)
}
log.Debugf("received connection confirmation from peer %s running version %s and with remote WireGuard listen port %d",
conn.config.Key, remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort)
// at this point we received offer/answer and we are ready to gather candidates
conn.mu.Lock()
conn.status = StatusConnecting
conn.ctx, conn.notifyDisconnected = context.WithCancel(context.Background())
defer conn.notifyDisconnected()
conn.mu.Unlock()
peerState = State{PubKey: conn.config.Key}
peerState.ConnStatus = conn.status
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
}
// will block until connection succeeded
// but it won't release if ICE Agent went into Disconnected or Failed state,
// so we have to cancel it with the provided context once agent detected a broken connection
isControlling := conn.config.LocalKey > conn.config.Key
var remoteConn *ice.Conn
if isControlling {
remoteConn, err = conn.agent.Dial(conn.ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
} else {
remoteConn, err = conn.agent.Accept(conn.ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
}
if err != nil {
return err
}
// dynamically set remote WireGuard port is other side specified a different one from the default one
remoteWgPort := iface.DefaultWgPort
if remoteOfferAnswer.WgListenPort != 0 {
remoteWgPort = remoteOfferAnswer.WgListenPort
}
// the ice connection has been established successfully so we are ready to start the proxy
err = conn.startProxy(remoteConn, remoteWgPort)
if err != nil {
return err
}
if conn.proxy.Type() == proxy.TypeDirectNoProxy {
host, _, _ := net.SplitHostPort(remoteConn.LocalAddr().String())
rhost, _, _ := net.SplitHostPort(remoteConn.RemoteAddr().String())
// direct Wireguard connection
log.Infof("directly connected to peer %s [laddr <-> raddr] [%s:%d <-> %s:%d]", conn.config.Key, host, conn.config.LocalWgPort, rhost, remoteWgPort)
} else {
log.Infof("connected to peer %s [laddr <-> raddr] [%s <-> %s]", conn.config.Key, remoteConn.LocalAddr().String(), remoteConn.RemoteAddr().String())
}
// wait until connection disconnected or has been closed externally (upper layer, e.g. engine)
select {
case <-conn.closeCh:
// closed externally
return NewConnectionClosedError(conn.config.Key)
case <-conn.ctx.Done():
// disconnected from the remote peer
return NewConnectionDisconnectedError(conn.config.Key)
}
}
// useProxy determines whether a direct connection (without a go proxy) is possible
//
// There are 3 cases:
//
// * When neither candidate is from hard nat and one of the peers has a public IP
//
// * both peers are in the same private network
//
// * Local peer uses userspace interface with bind.ICEBind and is not relayed
//
// Please note, that this check happens when peers were already able to ping each other using ICE layer.
func shouldUseProxy(pair *ice.CandidatePair, userspaceBind bool) bool {
if !isRelayCandidate(pair.Local) && userspaceBind {
log.Debugf("shouldn't use proxy because using Bind and the connection is not relayed")
return false
}
if !isHardNATCandidate(pair.Local) && isHostCandidateWithPublicIP(pair.Remote) {
log.Debugf("shouldn't use proxy because the local peer is not behind a hard NAT and the remote one has a public IP")
return false
}
if !isHardNATCandidate(pair.Remote) && isHostCandidateWithPublicIP(pair.Local) {
log.Debugf("shouldn't use proxy because the remote peer is not behind a hard NAT and the local one has a public IP")
return false
}
if isHostCandidateWithPrivateIP(pair.Local) && isHostCandidateWithPrivateIP(pair.Remote) && isSameNetworkPrefix(pair) {
log.Debugf("shouldn't use proxy because peers are in the same private /16 network")
return false
}
if (isPeerReflexiveCandidateWithPrivateIP(pair.Local) && isHostCandidateWithPrivateIP(pair.Remote) ||
isHostCandidateWithPrivateIP(pair.Local) && isPeerReflexiveCandidateWithPrivateIP(pair.Remote)) && isSameNetworkPrefix(pair) {
log.Debugf("shouldn't use proxy because peers are in the same private /16 network and one peer is peer reflexive")
return false
}
return true
}
func isSameNetworkPrefix(pair *ice.CandidatePair) bool {
localIP := net.ParseIP(pair.Local.Address())
remoteIP := net.ParseIP(pair.Remote.Address())
if localIP == nil || remoteIP == nil {
return false
}
// only consider /16 networks
mask := net.IPMask{255, 255, 0, 0}
return localIP.Mask(mask).Equal(remoteIP.Mask(mask))
}
func isRelayCandidate(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeRelay
}
func isHardNATCandidate(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeRelay || candidate.Type() == ice.CandidateTypePeerReflexive
}
func isHostCandidateWithPublicIP(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeHost && isPublicIP(candidate.Address())
}
func isHostCandidateWithPrivateIP(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeHost && !isPublicIP(candidate.Address())
}
func isPeerReflexiveCandidateWithPrivateIP(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypePeerReflexive && !isPublicIP(candidate.Address())
}
func isPublicIP(address string) bool {
ip := net.ParseIP(address)
if ip == nil || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsPrivate() {
return false
}
return true
}
// startProxy starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
func (conn *Conn) startProxy(remoteConn net.Conn, remoteWgPort int) error {
conn.mu.Lock()
defer conn.mu.Unlock()
var pair *ice.CandidatePair
pair, err := conn.agent.GetSelectedCandidatePair()
if err != nil {
return err
}
peerState := State{PubKey: conn.config.Key}
p := conn.getProxyWithMessageExchange(pair, remoteWgPort)
conn.proxy = p
err = p.Start(remoteConn)
if err != nil {
return err
}
conn.status = StatusConnected
peerState.ConnStatus = conn.status
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
}
peerState.Direct = p.Type() == proxy.TypeDirectNoProxy || p.Type() == proxy.TypeNoProxy
err = conn.statusRecorder.UpdatePeerState(peerState)
if err != nil {
log.Warnf("unable to save peer's state, got error: %v", err)
}
return nil
}
func (conn *Conn) getProxyWithMessageExchange(pair *ice.CandidatePair, remoteWgPort int) proxy.Proxy {
useProxy := shouldUseProxy(pair, conn.config.UserspaceBind)
localDirectMode := !useProxy
remoteDirectMode := localDirectMode
if conn.meta.protoSupport.DirectCheck {
go conn.sendLocalDirectMode(localDirectMode)
// will block until message received or timeout
remoteDirectMode = conn.receiveRemoteDirectMode()
}
if conn.config.UserspaceBind && localDirectMode {
return proxy.NewNoProxy(conn.config.ProxyConfig)
}
if localDirectMode && remoteDirectMode {
return proxy.NewDirectNoProxy(conn.config.ProxyConfig, remoteWgPort)
}
log.Debugf("falling back to local proxy mode with peer %s", conn.config.Key)
return proxy.NewWireGuardProxy(conn.config.ProxyConfig)
}
func (conn *Conn) sendLocalDirectMode(localMode bool) {
// todo what happens when we couldn't deliver this message?
// we could retry, etc but there is no guarantee
err := conn.sendSignalMessage(&sProto.Message{
Key: conn.config.LocalKey,
RemoteKey: conn.config.Key,
Body: &sProto.Body{
Type: sProto.Body_MODE,
Mode: &sProto.Mode{
Direct: &localMode,
},
NetBirdVersion: version.NetbirdVersion(),
},
})
if err != nil {
log.Errorf("failed to send local proxy mode to remote peer %s, error: %s", conn.config.Key, err)
}
}
func (conn *Conn) receiveRemoteDirectMode() bool {
timeout := time.Second
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case receivedMSG := <-conn.remoteModeCh:
return receivedMSG.Direct
case <-timer.C:
// we didn't receive a message from remote so we assume that it supports the direct mode to keep the old behaviour
log.Debugf("timeout after %s while waiting for remote direct mode message from remote peer %s",
timeout, conn.config.Key)
return true
}
}
// cleanup closes all open resources and sets status to StatusDisconnected
func (conn *Conn) cleanup() error {
log.Debugf("trying to cleanup %s", conn.config.Key)
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.agent != nil {
err := conn.agent.Close()
if err != nil {
return err
}
conn.agent = nil
}
if conn.proxy != nil {
err := conn.proxy.Close()
if err != nil {
return err
}
conn.proxy = nil
}
if conn.notifyDisconnected != nil {
conn.notifyDisconnected()
conn.notifyDisconnected = nil
}
conn.status = StatusDisconnected
peerState := State{PubKey: conn.config.Key}
peerState.ConnStatus = conn.status
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
}
// SetSignalOffer sets a handler function to be triggered by Conn when a new connection offer has to be signalled to the remote peer
func (conn *Conn) SetSignalOffer(handler func(offer OfferAnswer) error) {
conn.signalOffer = handler
}
// SetSignalAnswer sets a handler function to be triggered by Conn when a new connection answer has to be signalled to the remote peer
func (conn *Conn) SetSignalAnswer(handler func(answer OfferAnswer) error) {
conn.signalAnswer = handler
}
// SetSignalCandidate sets a handler function to be triggered by Conn when a new ICE local connection candidate has to be signalled to the remote peer
func (conn *Conn) SetSignalCandidate(handler func(candidate ice.Candidate) error) {
conn.signalCandidate = handler
}
// SetSendSignalMessage sets a handler function to be triggered by Conn when there is new message to send via signal
func (conn *Conn) SetSendSignalMessage(handler func(message *sProto.Message) error) {
conn.sendSignalMessage = handler
}
// onICECandidate is a callback attached to an ICE Agent to receive new local connection candidates
// and then signals them to the remote peer
func (conn *Conn) onICECandidate(candidate ice.Candidate) {
if candidate != nil {
// TODO: reported port is incorrect for CandidateTypeHost, makes understanding ICE use via logs confusing as port is ignored
log.Debugf("discovered local candidate %s", candidate.String())
go func() {
err := conn.signalCandidate(candidate)
if err != nil {
log.Errorf("failed signaling candidate to the remote peer %s %s", conn.config.Key, err)
}
}()
}
}
func (conn *Conn) onICESelectedCandidatePair(c1 ice.Candidate, c2 ice.Candidate) {
log.Debugf("selected candidate pair [local <-> remote] -> [%s <-> %s], peer %s", c1.String(), c2.String(),
conn.config.Key)
}
// onICEConnectionStateChange registers callback of an ICE Agent to track connection state
func (conn *Conn) onICEConnectionStateChange(state ice.ConnectionState) {
log.Debugf("peer %s ICE ConnectionState has changed to %s", conn.config.Key, state.String())
if state == ice.ConnectionStateFailed || state == ice.ConnectionStateDisconnected {
conn.notifyDisconnected()
}
}
func (conn *Conn) sendAnswer() error {
conn.mu.Lock()
defer conn.mu.Unlock()
localUFrag, localPwd, err := conn.agent.GetLocalUserCredentials()
if err != nil {
return err
}
log.Debugf("sending answer to %s", conn.config.Key)
err = conn.signalAnswer(OfferAnswer{
IceCredentials: IceCredentials{localUFrag, localPwd},
WgListenPort: conn.config.LocalWgPort,
Version: version.NetbirdVersion(),
})
if err != nil {
return err
}
return nil
}
// sendOffer prepares local user credentials and signals them to the remote peer
func (conn *Conn) sendOffer() error {
conn.mu.Lock()
defer conn.mu.Unlock()
localUFrag, localPwd, err := conn.agent.GetLocalUserCredentials()
if err != nil {
return err
}
err = conn.signalOffer(OfferAnswer{
IceCredentials: IceCredentials{localUFrag, localPwd},
WgListenPort: conn.config.LocalWgPort,
Version: version.NetbirdVersion(),
})
if err != nil {
return err
}
return nil
}
// Close closes this peer Conn issuing a close event to the Conn closeCh
func (conn *Conn) Close() error {
conn.mu.Lock()
defer conn.mu.Unlock()
select {
case conn.closeCh <- struct{}{}:
return nil
default:
// probably could happen when peer has been added and removed right after not even starting to connect
// todo further investigate
// this really happens due to unordered messages coming from management
// more importantly it causes inconsistency -> 2 Conn objects for the same peer
// e.g. this flow:
// update from management has peers: [1,2,3,4]
// engine creates a Conn for peers: [1,2,3,4] and schedules Open in ~1sec
// before conn.Open() another update from management arrives with peers: [1,2,3]
// engine removes peer 4 and calls conn.Close() which does nothing (this default clause)
// before conn.Open() another update from management arrives with peers: [1,2,3,4,5]
// engine adds a new Conn for 4 and 5
// therefore peer 4 has 2 Conn objects
log.Warnf("connection has been already closed or attempted closing not started coonection %s", conn.config.Key)
return NewConnectionAlreadyClosed(conn.config.Key)
}
}
// Status returns current status of the Conn
func (conn *Conn) Status() ConnStatus {
conn.mu.Lock()
defer conn.mu.Unlock()
return conn.status
}
// OnRemoteOffer handles an offer from the remote peer and returns true if the message was accepted, false otherwise
// doesn't block, discards the message if connection wasn't ready
func (conn *Conn) OnRemoteOffer(offer OfferAnswer) bool {
log.Debugf("OnRemoteOffer from peer %s on status %s", conn.config.Key, conn.status.String())
select {
case conn.remoteOffersCh <- offer:
return true
default:
log.Debugf("OnRemoteOffer skipping message from peer %s on status %s because is not ready", conn.config.Key, conn.status.String())
// connection might not be ready yet to receive so we ignore the message
return false
}
}
// OnRemoteAnswer handles an offer from the remote peer and returns true if the message was accepted, false otherwise
// doesn't block, discards the message if connection wasn't ready
func (conn *Conn) OnRemoteAnswer(answer OfferAnswer) bool {
log.Debugf("OnRemoteAnswer from peer %s on status %s", conn.config.Key, conn.status.String())
select {
case conn.remoteAnswerCh <- answer:
return true
default:
// connection might not be ready yet to receive so we ignore the message
log.Debugf("OnRemoteAnswer skipping message from peer %s on status %s because is not ready", conn.config.Key, conn.status.String())
return false
}
}
// OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer.
func (conn *Conn) OnRemoteCandidate(candidate ice.Candidate) {
log.Debugf("OnRemoteCandidate from peer %s -> %s", conn.config.Key, candidate.String())
go func() {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.agent == nil {
return
}
err := conn.agent.AddRemoteCandidate(candidate)
if err != nil {
log.Errorf("error while handling remote candidate from peer %s", conn.config.Key)
return
}
}()
}
func (conn *Conn) GetKey() string {
return conn.config.Key
}
// OnModeMessage unmarshall the payload message and send it to the mode message channel
func (conn *Conn) OnModeMessage(message ModeMessage) error {
select {
case conn.remoteModeCh <- message:
return nil
default:
return fmt.Errorf("unable to process mode message: channel busy")
}
}
// RegisterProtoSupportMeta register supported proto message in the connection metadata
func (conn *Conn) RegisterProtoSupportMeta(support []uint32) {
protoSupport := signal.ParseFeaturesSupported(support)
conn.meta.protoSupport = protoSupport
}

View File

@@ -1,29 +0,0 @@
package peer
import log "github.com/sirupsen/logrus"
const (
// StatusConnected indicate the peer is in connected state
StatusConnected ConnStatus = iota
// StatusConnecting indicate the peer is in connecting state
StatusConnecting
// StatusDisconnected indicate the peer is in disconnected state
StatusDisconnected
)
// ConnStatus describe the status of a peer's connection
type ConnStatus int
func (s ConnStatus) String() string {
switch s {
case StatusConnecting:
return "Connecting"
case StatusConnected:
return "Connected"
case StatusDisconnected:
return "Disconnected"
default:
log.Errorf("unknown status: %d", s)
return "INVALID_PEER_CONNECTION_STATUS"
}
}

View File

@@ -1,27 +0,0 @@
package peer
import (
"github.com/magiconair/properties/assert"
"testing"
)
func TestConnStatus_String(t *testing.T) {
tables := []struct {
name string
status ConnStatus
want string
}{
{"StatusConnected", StatusConnected, "Connected"},
{"StatusDisconnected", StatusDisconnected, "Disconnected"},
{"StatusConnecting", StatusConnecting, "Connecting"},
}
for _, table := range tables {
t.Run(table.name, func(t *testing.T) {
got := table.status.String()
assert.Equal(t, got, table.want, "they should be equal")
})
}
}

View File

@@ -1,479 +0,0 @@
package peer
import (
"sync"
"testing"
"time"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/magiconair/properties/assert"
"github.com/pion/ice/v2"
"golang.org/x/sync/errgroup"
"github.com/netbirdio/netbird/client/internal/proxy"
"github.com/netbirdio/netbird/iface"
sproto "github.com/netbirdio/netbird/signal/proto"
)
var connConf = ConnConfig{
Key: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
LocalKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
StunTurn: []*ice.URL{},
InterfaceBlackList: nil,
Timeout: time.Second,
ProxyConfig: proxy.Config{},
LocalWgPort: 51820,
}
func TestNewConn_interfaceFilter(t *testing.T) {
ignore := []string{iface.WgInterfaceDefault, "tun0", "zt", "ZeroTier", "utun", "wg", "ts",
"Tailscale", "tailscale"}
filter := stdnet.InterfaceFilter(ignore)
for _, s := range ignore {
assert.Equal(t, filter(s), false)
}
}
func TestConn_GetKey(t *testing.T) {
conn, err := NewConn(connConf, nil, nil, nil)
if err != nil {
return
}
got := conn.GetKey()
assert.Equal(t, got, connConf.Key, "they should be equal")
}
func TestConn_OnRemoteOffer(t *testing.T) {
conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil)
if err != nil {
return
}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
<-conn.remoteOffersCh
wg.Done()
}()
go func() {
for {
accepted := conn.OnRemoteOffer(OfferAnswer{
IceCredentials: IceCredentials{
UFrag: "test",
Pwd: "test",
},
WgListenPort: 0,
Version: "",
})
if accepted {
wg.Done()
return
}
}
}()
wg.Wait()
}
func TestConn_OnRemoteAnswer(t *testing.T) {
conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil)
if err != nil {
return
}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
<-conn.remoteAnswerCh
wg.Done()
}()
go func() {
for {
accepted := conn.OnRemoteAnswer(OfferAnswer{
IceCredentials: IceCredentials{
UFrag: "test",
Pwd: "test",
},
WgListenPort: 0,
Version: "",
})
if accepted {
wg.Done()
return
}
}
}()
wg.Wait()
}
func TestConn_Status(t *testing.T) {
conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil)
if err != nil {
return
}
tables := []struct {
name string
status ConnStatus
want ConnStatus
}{
{"StatusConnected", StatusConnected, StatusConnected},
{"StatusDisconnected", StatusDisconnected, StatusDisconnected},
{"StatusConnecting", StatusConnecting, StatusConnecting},
}
for _, table := range tables {
t.Run(table.name, func(t *testing.T) {
conn.status = table.status
got := conn.Status()
assert.Equal(t, got, table.want, "they should be equal")
})
}
}
func TestConn_Close(t *testing.T) {
conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil)
if err != nil {
return
}
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
<-conn.closeCh
wg.Done()
}()
go func() {
for {
err := conn.Close()
if err != nil {
continue
} else {
return
}
}
}()
wg.Wait()
}
type mockICECandidate struct {
ice.Candidate
AddressFunc func() string
TypeFunc func() ice.CandidateType
}
// Address mocks and overwrite ice.Candidate Address method
func (m *mockICECandidate) Address() string {
if m.AddressFunc != nil {
return m.AddressFunc()
}
return ""
}
// Type mocks and overwrite ice.Candidate Type method
func (m *mockICECandidate) Type() ice.CandidateType {
if m.TypeFunc != nil {
return m.TypeFunc()
}
return ice.CandidateTypeUnspecified
}
func TestConn_ShouldUseProxy(t *testing.T) {
publicHostCandidate := &mockICECandidate{
AddressFunc: func() string {
return "8.8.8.8"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeHost
},
}
privateHostCandidate := &mockICECandidate{
AddressFunc: func() string {
return "10.0.0.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeHost
},
}
srflxCandidate := &mockICECandidate{
AddressFunc: func() string {
return "1.1.1.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeServerReflexive
},
}
prflxCandidate := &mockICECandidate{
AddressFunc: func() string {
return "1.1.1.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypePeerReflexive
},
}
relayCandidate := &mockICECandidate{
AddressFunc: func() string {
return "1.1.1.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeRelay
},
}
testCases := []struct {
name string
candatePair *ice.CandidatePair
expected bool
}{
{
name: "Use Proxy When Local Candidate Is Relay",
candatePair: &ice.CandidatePair{
Local: relayCandidate,
Remote: privateHostCandidate,
},
expected: true,
},
{
name: "Use Proxy When Remote Candidate Is Relay",
candatePair: &ice.CandidatePair{
Local: privateHostCandidate,
Remote: relayCandidate,
},
expected: true,
},
{
name: "Use Proxy When Local Candidate Is Peer Reflexive",
candatePair: &ice.CandidatePair{
Local: prflxCandidate,
Remote: privateHostCandidate,
},
expected: true,
},
{
name: "Use Proxy When Remote Candidate Is Peer Reflexive",
candatePair: &ice.CandidatePair{
Local: privateHostCandidate,
Remote: prflxCandidate,
},
expected: true,
},
{
name: "Don't Use Proxy When Local Candidate Is Public And Remote Is Private",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: privateHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Remote Candidate Is Public And Local Is Private",
candatePair: &ice.CandidatePair{
Local: privateHostCandidate,
Remote: publicHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Local Candidate is Public And Remote Is Server Reflexive",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: srflxCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Remote Candidate is Public And Local Is Server Reflexive",
candatePair: &ice.CandidatePair{
Local: srflxCandidate,
Remote: publicHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Both Candidates Are Public",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: publicHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Both Candidates Are Private",
candatePair: &ice.CandidatePair{
Local: privateHostCandidate,
Remote: privateHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Both Candidates are in private network and one is peer reflexive",
candatePair: &ice.CandidatePair{
Local: &mockICECandidate{AddressFunc: func() string {
return "10.16.102.168"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeHost
}},
Remote: &mockICECandidate{AddressFunc: func() string {
return "10.16.101.96"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypePeerReflexive
}},
},
expected: false,
},
{
name: "Should Use Proxy When Both Candidates are in private network and both are peer reflexive",
candatePair: &ice.CandidatePair{
Local: &mockICECandidate{AddressFunc: func() string {
return "10.16.102.168"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypePeerReflexive
}},
Remote: &mockICECandidate{AddressFunc: func() string {
return "10.16.101.96"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypePeerReflexive
}},
},
expected: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result := shouldUseProxy(testCase.candatePair, false)
if result != testCase.expected {
t.Errorf("got a different result. Expected %t Got %t", testCase.expected, result)
}
})
}
}
func TestGetProxyWithMessageExchange(t *testing.T) {
publicHostCandidate := &mockICECandidate{
AddressFunc: func() string {
return "8.8.8.8"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeHost
},
}
relayCandidate := &mockICECandidate{
AddressFunc: func() string {
return "1.1.1.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeRelay
},
}
testCases := []struct {
name string
candatePair *ice.CandidatePair
inputDirectModeSupport bool
inputRemoteModeMessage bool
expected proxy.Type
}{
{
name: "Should Result In Using Wireguard Proxy When Local Eval Is Use Proxy",
candatePair: &ice.CandidatePair{
Local: relayCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: true,
inputRemoteModeMessage: true,
expected: proxy.TypeWireGuard,
},
{
name: "Should Result In Using Wireguard Proxy When Remote Eval Is Use Proxy",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: true,
inputRemoteModeMessage: false,
expected: proxy.TypeWireGuard,
},
{
name: "Should Result In Using Wireguard Proxy When Remote Direct Mode Support Is False And Local Eval Is Use Proxy",
candatePair: &ice.CandidatePair{
Local: relayCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: false,
inputRemoteModeMessage: false,
expected: proxy.TypeWireGuard,
},
{
name: "Should Result In Using Direct When Remote Direct Mode Support Is False And Local Eval Is No Use Proxy",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: false,
inputRemoteModeMessage: false,
expected: proxy.TypeDirectNoProxy,
},
{
name: "Should Result In Using Direct When Local And Remote Eval Is No Proxy",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: true,
inputRemoteModeMessage: true,
expected: proxy.TypeDirectNoProxy,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
g := errgroup.Group{}
conn, err := NewConn(connConf, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
conn.meta.protoSupport.DirectCheck = testCase.inputDirectModeSupport
conn.SetSendSignalMessage(func(message *sproto.Message) error {
return nil
})
g.Go(func() error {
return conn.OnModeMessage(ModeMessage{
Direct: testCase.inputRemoteModeMessage,
})
})
resultProxy := conn.getProxyWithMessageExchange(testCase.candatePair, 1000)
err = g.Wait()
if err != nil {
t.Error(err)
}
if resultProxy.Type() != testCase.expected {
t.Errorf("result didn't match expected value: Expected: %s, Got: %s", testCase.expected, resultProxy.Type())
}
})
}
}

View File

@@ -1,72 +0,0 @@
package peer
import (
"fmt"
"time"
)
// ConnectionTimeoutError is an error indicating that a peer Conn has been timed out
type ConnectionTimeoutError struct {
peer string
timeout time.Duration
}
func (e *ConnectionTimeoutError) Error() string {
return fmt.Sprintf("connection to peer %s timed out after %s", e.peer, e.timeout.String())
}
// NewConnectionTimeoutError creates a new ConnectionTimeoutError error
func NewConnectionTimeoutError(peer string, timeout time.Duration) error {
return &ConnectionTimeoutError{
peer: peer,
timeout: timeout,
}
}
// ConnectionClosedError is an error indicating that a peer Conn has been forcefully closed
type ConnectionClosedError struct {
peer string
}
func (e *ConnectionClosedError) Error() string {
return fmt.Sprintf("connection to peer %s has been closed", e.peer)
}
// NewConnectionClosedError creates a new ConnectionClosedError error
func NewConnectionClosedError(peer string) error {
return &ConnectionClosedError{
peer: peer,
}
}
// ConnectionDisconnectedError is an error indicating that a peer Conn has ctx from the remote
type ConnectionDisconnectedError struct {
peer string
}
func (e *ConnectionDisconnectedError) Error() string {
return fmt.Sprintf("disconnected from peer %s", e.peer)
}
// NewConnectionDisconnectedError creates a new ConnectionDisconnectedError error
func NewConnectionDisconnectedError(peer string) error {
return &ConnectionDisconnectedError{
peer: peer,
}
}
// ConnectionAlreadyClosedError is an error indicating that a peer Conn has been already closed and the invocation of the Close() method has been performed over a closed connection
type ConnectionAlreadyClosedError struct {
peer string
}
func (e *ConnectionAlreadyClosedError) Error() string {
return fmt.Sprintf("connection to peer %s has been already closed", e.peer)
}
// NewConnectionAlreadyClosed creates a new ConnectionAlreadyClosedError error
func NewConnectionAlreadyClosed(peer string) error {
return &ConnectionAlreadyClosedError{
peer: peer,
}
}

View File

@@ -1,27 +0,0 @@
package peer
import (
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestNewConnectionClosedError(t *testing.T) {
err := NewConnectionClosedError("X")
assert.Equal(t, &ConnectionClosedError{peer: "X"}, err)
}
func TestNewConnectionDisconnectedError(t *testing.T) {
err := NewConnectionDisconnectedError("X")
assert.Equal(t, &ConnectionDisconnectedError{peer: "X"}, err)
}
func TestNewConnectionTimeoutErrorC(t *testing.T) {
err := NewConnectionTimeoutError("X", time.Second)
assert.Equal(t, &ConnectionTimeoutError{peer: "X", timeout: time.Second}, err)
}
func TestNewConnectionAlreadyClosed(t *testing.T) {
err := NewConnectionAlreadyClosed("X")
assert.Equal(t, &ConnectionAlreadyClosedError{peer: "X"}, err)
}

View File

@@ -1,11 +0,0 @@
package peer
// Listener is a callback type about the NetBird network connection state
type Listener interface {
OnConnected()
OnDisconnected()
OnConnecting()
OnDisconnecting()
OnAddressChanged(string, string)
OnPeersListChanged(int)
}

View File

@@ -1,142 +0,0 @@
package peer
import (
"sync"
)
const (
stateDisconnected = iota
stateConnected
stateConnecting
stateDisconnecting
)
type notifier struct {
serverStateLock sync.Mutex
listenersLock sync.Mutex
listener Listener
currentClientState bool
lastNotification int
}
func newNotifier() *notifier {
return &notifier{}
}
func (n *notifier) setListener(listener Listener) {
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
n.serverStateLock.Lock()
n.notifyListener(listener, n.lastNotification)
n.serverStateLock.Unlock()
n.listener = listener
}
func (n *notifier) removeListener() {
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
n.listener = nil
}
func (n *notifier) updateServerStates(mgmState bool, signalState bool) {
n.serverStateLock.Lock()
defer n.serverStateLock.Unlock()
calculatedState := n.calculateState(mgmState, signalState)
if !n.isServerStateChanged(calculatedState) {
return
}
n.lastNotification = calculatedState
n.notify(n.lastNotification)
}
func (n *notifier) clientStart() {
n.serverStateLock.Lock()
defer n.serverStateLock.Unlock()
n.currentClientState = true
n.lastNotification = stateConnected
n.notify(n.lastNotification)
}
func (n *notifier) clientStop() {
n.serverStateLock.Lock()
defer n.serverStateLock.Unlock()
n.currentClientState = false
n.lastNotification = stateDisconnected
n.notify(n.lastNotification)
}
func (n *notifier) clientTearDown() {
n.serverStateLock.Lock()
defer n.serverStateLock.Unlock()
n.currentClientState = false
n.lastNotification = stateDisconnecting
n.notify(n.lastNotification)
}
func (n *notifier) isServerStateChanged(newState int) bool {
return n.lastNotification != newState
}
func (n *notifier) notify(state int) {
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
if n.listener == nil {
return
}
n.notifyListener(n.listener, state)
}
func (n *notifier) notifyListener(l Listener, state int) {
go func() {
switch state {
case stateDisconnected:
l.OnDisconnected()
case stateConnected:
l.OnConnected()
case stateConnecting:
l.OnConnecting()
case stateDisconnecting:
l.OnDisconnecting()
}
}()
}
func (n *notifier) calculateState(managementConn, signalConn bool) int {
if managementConn && signalConn {
return stateConnected
}
if !managementConn && !signalConn {
return stateDisconnected
}
if n.lastNotification == stateDisconnecting {
return stateDisconnecting
}
return stateConnecting
}
func (n *notifier) peerListChanged(numOfPeers int) {
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
if n.listener == nil {
return
}
n.listener.OnPeersListChanged(numOfPeers)
}
func (n *notifier) localAddressChanged(fqdn, address string) {
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
if n.listener == nil {
return
}
n.listener.OnAddressChanged(fqdn, address)
}

View File

@@ -1,97 +0,0 @@
package peer
import (
"sync"
"testing"
)
type mocListener struct {
lastState int
wg sync.WaitGroup
peers int
}
func (l *mocListener) OnConnected() {
l.lastState = stateConnected
l.wg.Done()
}
func (l *mocListener) OnDisconnected() {
l.lastState = stateDisconnected
l.wg.Done()
}
func (l *mocListener) OnConnecting() {
l.lastState = stateConnecting
l.wg.Done()
}
func (l *mocListener) OnDisconnecting() {
l.lastState = stateDisconnecting
l.wg.Done()
}
func (l *mocListener) OnAddressChanged(host, addr string) {
}
func (l *mocListener) OnPeersListChanged(size int) {
l.peers = size
}
func (l *mocListener) setWaiter() {
l.wg.Add(1)
}
func (l *mocListener) wait() {
l.wg.Wait()
}
func Test_notifier_serverState(t *testing.T) {
type scenario struct {
name string
expected int
mgmState bool
signalState bool
}
scenarios := []scenario{
{"connected", stateConnected, true, true},
{"mgm down", stateConnecting, false, true},
{"signal down", stateConnecting, true, false},
{"disconnected", stateDisconnected, false, false},
}
for _, tt := range scenarios {
t.Run(tt.name, func(t *testing.T) {
n := newNotifier()
n.updateServerStates(tt.mgmState, tt.signalState)
if n.lastNotification != tt.expected {
t.Errorf("invalid serverstate: %d, expected: %d", n.lastNotification, tt.expected)
}
})
}
}
func Test_notifier_SetListener(t *testing.T) {
listener := &mocListener{}
listener.setWaiter()
n := newNotifier()
n.lastNotification = stateConnecting
n.setListener(listener)
listener.wait()
if listener.lastState != n.lastNotification {
t.Errorf("invalid state: %d, expected: %d", listener.lastState, n.lastNotification)
}
}
func Test_notifier_RemoveListener(t *testing.T) {
listener := &mocListener{}
listener.setWaiter()
n := newNotifier()
n.lastNotification = stateConnecting
n.setListener(listener)
n.removeListener()
n.peerListChanged(1)
if listener.peers != 0 {
t.Errorf("invalid state: %d", listener.peers)
}
}

View File

@@ -1,316 +0,0 @@
package peer
import (
"errors"
"sync"
"time"
)
// State contains the latest state of a peer
type State struct {
IP string
PubKey string
FQDN string
ConnStatus ConnStatus
ConnStatusUpdate time.Time
Relayed bool
Direct bool
LocalIceCandidateType string
RemoteIceCandidateType string
}
// LocalPeerState contains the latest state of the local peer
type LocalPeerState struct {
IP string
PubKey string
KernelInterface bool
FQDN string
}
// SignalState contains the latest state of a signal connection
type SignalState struct {
URL string
Connected bool
}
// ManagementState contains the latest state of a management connection
type ManagementState struct {
URL string
Connected bool
}
// FullStatus contains the full state held by the Status instance
type FullStatus struct {
Peers []State
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]State
changeNotify map[string]chan struct{}
signalState bool
managementState bool
localPeer LocalPeerState
offlinePeers []State
mgmAddress string
signalAddress string
notifier *notifier
}
// NewRecorder returns a new Status instance
func NewRecorder(mgmAddress string) *Status {
return &Status{
peers: make(map[string]State),
changeNotify: make(map[string]chan struct{}),
offlinePeers: make([]State, 0),
notifier: newNotifier(),
mgmAddress: mgmAddress,
}
}
// ReplaceOfflinePeers replaces
func (d *Status) ReplaceOfflinePeers(replacement []State) {
d.mux.Lock()
defer d.mux.Unlock()
d.offlinePeers = make([]State, len(replacement))
copy(d.offlinePeers, replacement)
}
// 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] = State{PubKey: peerPubKey, ConnStatus: StatusDisconnected}
return nil
}
// GetPeer adds peer to Daemon status map
func (d *Status) GetPeer(peerPubKey string) (State, error) {
d.mux.Lock()
defer d.mux.Unlock()
state, ok := d.peers[peerPubKey]
if !ok {
return State{}, errors.New("peer not found")
}
return state, nil
}
// RemovePeer removes peer from Daemon status map
func (d *Status) RemovePeer(peerPubKey string) error {
d.mux.Lock()
defer d.mux.Unlock()
_, ok := d.peers[peerPubKey]
if ok {
delete(d.peers, peerPubKey)
return nil
}
d.notifyPeerListChanged()
return errors.New("no peer with to remove")
}
// UpdatePeerState updates peer status
func (d *Status) UpdatePeerState(receivedState State) error {
d.mux.Lock()
defer d.mux.Unlock()
peerState, ok := d.peers[receivedState.PubKey]
if !ok {
return errors.New("peer doesn't exist")
}
if receivedState.IP != "" {
peerState.IP = receivedState.IP
}
if receivedState.ConnStatus != peerState.ConnStatus {
peerState.ConnStatus = receivedState.ConnStatus
peerState.ConnStatusUpdate = receivedState.ConnStatusUpdate
peerState.Direct = receivedState.Direct
peerState.Relayed = receivedState.Relayed
peerState.LocalIceCandidateType = receivedState.LocalIceCandidateType
peerState.RemoteIceCandidateType = receivedState.RemoteIceCandidateType
}
d.peers[receivedState.PubKey] = peerState
ch, found := d.changeNotify[receivedState.PubKey]
if found && ch != nil {
close(ch)
d.changeNotify[receivedState.PubKey] = nil
}
d.notifyPeerListChanged()
return nil
}
// UpdatePeerFQDN update peer's state fqdn only
func (d *Status) UpdatePeerFQDN(peerPubKey, fqdn string) error {
d.mux.Lock()
defer d.mux.Unlock()
peerState, ok := d.peers[peerPubKey]
if !ok {
return errors.New("peer doesn't exist")
}
peerState.FQDN = fqdn
d.peers[peerPubKey] = peerState
d.notifyPeerListChanged()
return nil
}
// GetPeerStateChangeNotifier returns a change notifier channel for a peer
func (d *Status) GetPeerStateChangeNotifier(peer string) <-chan struct{} {
d.mux.Lock()
defer d.mux.Unlock()
ch, found := d.changeNotify[peer]
if !found || ch == nil {
ch = make(chan struct{})
d.changeNotify[peer] = ch
}
return ch
}
// UpdateLocalPeerState updates local peer status
func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) {
d.mux.Lock()
defer d.mux.Unlock()
d.localPeer = localPeerState
d.notifyAddressChanged()
}
// CleanLocalPeerState cleans local peer status
func (d *Status) CleanLocalPeerState() {
d.mux.Lock()
defer d.mux.Unlock()
d.localPeer = LocalPeerState{}
d.notifyAddressChanged()
}
// MarkManagementDisconnected sets ManagementState to disconnected
func (d *Status) MarkManagementDisconnected() {
d.mux.Lock()
defer d.mux.Unlock()
defer d.onConnectionChanged()
d.managementState = false
}
// MarkManagementConnected sets ManagementState to connected
func (d *Status) MarkManagementConnected() {
d.mux.Lock()
defer d.mux.Unlock()
defer d.onConnectionChanged()
d.managementState = true
}
// UpdateSignalAddress update the address of the signal server
func (d *Status) UpdateSignalAddress(signalURL string) {
d.mux.Lock()
defer d.mux.Unlock()
d.signalAddress = signalURL
}
// UpdateManagementAddress update the address of the management server
func (d *Status) UpdateManagementAddress(mgmAddress string) {
d.mux.Lock()
defer d.mux.Unlock()
d.mgmAddress = mgmAddress
}
// MarkSignalDisconnected sets SignalState to disconnected
func (d *Status) MarkSignalDisconnected() {
d.mux.Lock()
defer d.mux.Unlock()
defer d.onConnectionChanged()
d.signalState = false
}
// MarkSignalConnected sets SignalState to connected
func (d *Status) MarkSignalConnected() {
d.mux.Lock()
defer d.mux.Unlock()
defer d.onConnectionChanged()
d.signalState = true
}
// GetFullStatus gets full status
func (d *Status) GetFullStatus() FullStatus {
d.mux.Lock()
defer d.mux.Unlock()
fullStatus := FullStatus{
ManagementState: ManagementState{
d.mgmAddress,
d.managementState,
},
SignalState: SignalState{
d.signalAddress,
d.signalState,
},
LocalPeerState: d.localPeer,
}
for _, status := range d.peers {
fullStatus.Peers = append(fullStatus.Peers, status)
}
fullStatus.Peers = append(fullStatus.Peers, d.offlinePeers...)
return fullStatus
}
// ClientStart will notify all listeners about the new service state
func (d *Status) ClientStart() {
d.notifier.clientStart()
}
// ClientStop will notify all listeners about the new service state
func (d *Status) ClientStop() {
d.notifier.clientStop()
}
// ClientTeardown will notify all listeners about the service is under teardown
func (d *Status) ClientTeardown() {
d.notifier.clientTearDown()
}
// SetConnectionListener set a listener to the notifier
func (d *Status) SetConnectionListener(listener Listener) {
d.notifier.setListener(listener)
}
// RemoveConnectionListener remove the listener from the notifier
func (d *Status) RemoveConnectionListener() {
d.notifier.removeListener()
}
func (d *Status) onConnectionChanged() {
d.notifier.updateServerStates(d.managementState, d.signalState)
}
func (d *Status) notifyPeerListChanged() {
d.notifier.peerListChanged(len(d.peers))
}
func (d *Status) notifyAddressChanged() {
d.notifier.localAddressChanged(d.localPeer.FQDN, d.localPeer.IP)
}

View File

@@ -1,233 +0,0 @@
package peer
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAddPeer(t *testing.T) {
key := "abc"
status := NewRecorder("https://mgm")
err := status.AddPeer(key)
assert.NoError(t, err, "shouldn't return error")
_, exists := status.peers[key]
assert.True(t, exists, "value was found")
err = status.AddPeer(key)
assert.Error(t, err, "should return error on duplicate")
}
func TestGetPeer(t *testing.T) {
key := "abc"
status := NewRecorder("https://mgm")
err := status.AddPeer(key)
assert.NoError(t, err, "shouldn't return error")
peerStatus, err := status.GetPeer(key)
assert.NoError(t, err, "shouldn't return error on getting peer")
assert.Equal(t, key, peerStatus.PubKey, "retrieved public key should match")
_, err = status.GetPeer("non_existing_key")
assert.Error(t, err, "should return error when peer doesn't exist")
}
func TestUpdatePeerState(t *testing.T) {
key := "abc"
ip := "10.10.10.10"
status := NewRecorder("https://mgm")
peerState := State{
PubKey: key,
}
status.peers[key] = peerState
peerState.IP = ip
err := status.UpdatePeerState(peerState)
assert.NoError(t, err, "shouldn't return error")
state, exists := status.peers[key]
assert.True(t, exists, "state should be found")
assert.Equal(t, ip, state.IP, "ip should be equal")
}
func TestStatus_UpdatePeerFQDN(t *testing.T) {
key := "abc"
fqdn := "peer-a.netbird.local"
status := NewRecorder("https://mgm")
peerState := State{
PubKey: key,
}
status.peers[key] = peerState
err := status.UpdatePeerFQDN(key, fqdn)
assert.NoError(t, err, "shouldn't return error")
state, exists := status.peers[key]
assert.True(t, exists, "state should be found")
assert.Equal(t, fqdn, state.FQDN, "fqdn should be equal")
}
func TestGetPeerStateChangeNotifierLogic(t *testing.T) {
key := "abc"
ip := "10.10.10.10"
status := NewRecorder("https://mgm")
peerState := State{
PubKey: key,
}
status.peers[key] = peerState
ch := status.GetPeerStateChangeNotifier(key)
assert.NotNil(t, ch, "channel shouldn't be nil")
peerState.IP = ip
err := status.UpdatePeerState(peerState)
assert.NoError(t, err, "shouldn't return error")
select {
case <-ch:
default:
t.Errorf("channel wasn't closed after update")
}
}
func TestRemovePeer(t *testing.T) {
key := "abc"
status := NewRecorder("https://mgm")
peerState := State{
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("https://mgm")
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("https://mgm")
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 bool
}{
{"should mark as connected", true, true},
{"should mark as disconnected", false, false},
}
status := NewRecorder("https://mgm")
status.UpdateSignalAddress(url)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.connected {
status.MarkSignalConnected()
} else {
status.MarkSignalDisconnected()
}
assert.Equal(t, test.want, status.signalState, "signal status should be equal")
})
}
}
func TestUpdateManagementState(t *testing.T) {
url := "https://management"
var tests = []struct {
name string
connected bool
want bool
}{
{"should mark as connected", true, true},
{"should mark as disconnected", false, false},
}
status := NewRecorder(url)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.connected {
status.MarkManagementConnected()
} else {
status.MarkManagementDisconnected()
}
assert.Equal(t, test.want, status.managementState, "signalState status should be equal")
})
}
}
func TestGetFullStatus(t *testing.T) {
key1 := "abc"
key2 := "def"
signalAddr := "https://signal"
managementState := ManagementState{
URL: "https://mgm",
Connected: true,
}
signalState := SignalState{
URL: signalAddr,
Connected: true,
}
peerState1 := State{
PubKey: key1,
}
peerState2 := State{
PubKey: key2,
}
status := NewRecorder("https://mgm")
status.UpdateSignalAddress(signalAddr)
status.managementState = managementState.Connected
status.signalState = signalState.Connected
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, []State{peerState1, peerState2}, fullStatus.Peers, "peers states should match")
}

View File

@@ -1,11 +0,0 @@
//go:build !android
package peer
import (
"github.com/netbirdio/netbird/client/internal/stdnet"
)
func (conn *Conn) newStdNet() (*stdnet.Net, error) {
return stdnet.NewNet(conn.config.InterfaceBlackList)
}

View File

@@ -1,7 +0,0 @@
package peer
import "github.com/netbirdio/netbird/client/internal/stdnet"
func (conn *Conn) newStdNet() (*stdnet.Net, error) {
return stdnet.NewNetWithDiscover(conn.iFaceDiscover, conn.config.InterfaceBlackList)
}

View File

@@ -1,57 +0,0 @@
package proxy
import (
log "github.com/sirupsen/logrus"
"net"
)
// DirectNoProxy is used when there is no need for a proxy between ICE and WireGuard.
// This is possible in either of these cases:
// - peers are in the same local network
// - one of the peers has a public static IP (host)
// DirectNoProxy will just update remote peer with a remote host and fixed WireGuard port (r.g. 51820).
// In order DirectNoProxy to work, WireGuard port has to be fixed for the time being.
type DirectNoProxy struct {
config Config
// RemoteWgListenPort is a WireGuard port of a remote peer.
// It is used instead of the hardcoded 51820 port.
RemoteWgListenPort int
}
// NewDirectNoProxy creates a new DirectNoProxy with a provided config and remote peer's WireGuard listen port
func NewDirectNoProxy(config Config, remoteWgPort int) *DirectNoProxy {
return &DirectNoProxy{config: config, RemoteWgListenPort: remoteWgPort}
}
// Close removes peer from the WireGuard interface
func (p *DirectNoProxy) Close() error {
err := p.config.WgInterface.RemovePeer(p.config.RemoteKey)
if err != nil {
return err
}
return nil
}
// Start just updates WireGuard peer with the remote IP and default WireGuard port
func (p *DirectNoProxy) Start(remoteConn net.Conn) error {
log.Debugf("using DirectNoProxy while connecting to peer %s", p.config.RemoteKey)
addr, err := net.ResolveUDPAddr("udp", remoteConn.RemoteAddr().String())
if err != nil {
return err
}
addr.Port = p.RemoteWgListenPort
err = p.config.WgInterface.UpdatePeer(p.config.RemoteKey, p.config.AllowedIps, DefaultWgKeepAlive,
addr, p.config.PreSharedKey)
if err != nil {
return err
}
return nil
}
// Type returns the type of this proxy
func (p *DirectNoProxy) Type() Type {
return TypeDirectNoProxy
}

Some files were not shown because too many files have changed in this diff Show More