Compare commits

...

57 Commits

Author SHA1 Message Date
Mikhail Bragin
6ae27c9a9b Refactor: support multiple users under the same account (#170)
* feature: add User entity to Account

* test: new file store creation test

* test: add FileStore persist-restore tests

* test: add GetOrCreateAccountByUser Accountmanager test

* refactor: rename account manager users file

* refactor: use userId instead of accountId when handling Management HTTP API

* fix: new account creation for every request

* fix: golint

* chore: add account creator to Account Entity to identify who created the account.

* chore: use xid ID generator for account IDs

* fix: test failures

* test: check that CreatedBy is stored when account is stored

* chore: add account copy method

* test: remove test for non existent GetOrCreateAccount func

* chore: add accounts conversion function

* fix: golint

* refactor: simplify admin user creation

* refactor: move migration script to a separate package
2021-12-27 13:17:15 +01:00
braginini
ff6e369a21 chore: explain why keeping service lib at specific version 2021-12-21 12:10:18 +01:00
braginini
5c3b5e7f40 fix: rollback kardianos pkg 2021-12-21 12:07:14 +01:00
Mikhail Bragin
8c75ef8bef update to go 1.17 (#167)
* chore: update to go 1.17

* fix: update workflows go version

* fix: golint errors/update grpc
2021-12-21 10:02:25 +01:00
Mikhail Bragin
fdc11fff47 update docs (#164) 2021-12-06 13:54:46 +01:00
Mikhail Bragin
3dca2d6953 Update README.md 2021-11-22 23:11:26 +01:00
Mikhail Bragin
6b7d4cf644 feature: add Wireguard preshared-key support (#160) 2021-11-21 17:47:19 +01:00
Mikhail Bragin
edd4125742 docs: simplify intro 2021-11-20 14:53:57 +01:00
Maycon Santos
7bf9793f85 Support environment vars (#155)
* updage flag values from environment variables

* add log and removing unused constants

* removing unused code

* Docker build client

* fix indentation

* Documentation with docker command

* use docker volume
2021-11-15 09:11:50 +01:00
Maycon Santos
fcbf980588 Stop service before uninstall (#158) 2021-11-14 21:30:18 +01:00
Mikhail Bragin
d08e5efbce fix: too many open files caused by agent not being closed (#154)
* fix: too many open files caused by agent not being closed after unsuccessful attempts to start a peer connection (happens when no network available)

* fix: minor refactor to consider signal status
2021-11-14 19:41:17 +01:00
Maycon Santos
95ef8547f3 Signal management arm builds (#152)
* Add arm builds for Signal and Management services

* adding arm's binary version
2021-11-07 13:11:03 +01:00
Mikhail Bragin
ed1e4dfc51 refactor signal client sync func (#147)
* refactor: move goroutine that runs Signal Client Receive to the engine for better control

* chore: fix comments typo

* test: fix golint

* chore: comments update

* chore: consider connection state=READY in signal and management clients

* chore: fix typos

* test: fix signal ping-pong test

* chore: add wait condition to signal client

* refactor: add stream status to the Signal client

* refactor: defer mutex unlock
2021-11-06 15:00:13 +01:00
braginini
4d34fb4e64 chore: decrease backoff maxinterval to avoid long connection waiting times on the client app 2021-11-02 14:51:29 +01:00
Maycon Santos
1fb8b74cd2 set IF arm6 and empty attribute for package (#146)
There is a behavior or bug in goreleaser where it appends the file name in the target URL and that was causing issues and misconfigured properties
2021-11-01 20:33:26 +01:00
Mikhail Bragin
d040cfed7e fix: client app retry logic (#144)
* fix: retry logic
2021-11-01 09:34:06 +01:00
Maycon Santos
2c729fe5cc remove architecture info from deb (#145) 2021-11-01 09:33:22 +01:00
braginini
e9066b4651 chore: increase signal and management gRPC clients timeouts 2021-10-31 12:14:00 +01:00
Mikhail Bragin
673e807528 chore: set default key expiration if not provided by frontednd (#142) 2021-10-31 12:06:44 +01:00
Mikhail Bragin
892080bc38 docs: update key features 2021-10-27 13:56:55 +02:00
braginini
2d39f6ccae fix: remove ICE port limits 2021-10-27 10:49:03 +02:00
Mikhail Bragin
0b2c26847b fix: return ctx error when UP command exists (#140) 2021-10-26 21:49:05 +02:00
braginini
595ea0d4f8 chore: decrease log verbosity 2021-10-26 10:08:28 +02:00
Maycon Santos
f714868fdd remove arch if and replacement for debian packages (#138) 2021-10-23 10:29:49 +02:00
Mikhail Bragin
81821a1f39 docs: update diagram and Wireguard title (#137)
* docs: update diagram and Wireguard title
2021-10-21 10:06:29 +02:00
mlsmaycon
842b143a48 sync go.sum 2021-10-20 21:57:16 +02:00
Maycon Santos
1323a74db0 fix: avoid failing and extra error messages (#136)
* avoid failing and extra error messages

* avoid extra error messages when executed after pre_remove.sh

* remove extra output and avoid failure on minor errors

* ensure the steps will run only on remove
2021-10-20 11:51:32 +02:00
Mikhail Bragin
74485d3b13 fix: service hanging when error on startup has been encountered (#135) 2021-10-18 13:29:26 +02:00
Mikhail Bragin
bef3b3392b fix: graceful shutdown (#134)
* fix: graceful shutdown

* fix: windows graceful shutdown
2021-10-17 22:15:38 +02:00
Maycon Santos
fcea3c99d4 Enhance up command (#133)
* move setup-key to root command

* up will check login and start service

* update tests to reflect new UP capabilities

* display client IP

* removed unused argument

* install service if not installed

* update post-install and add pre remove script

* improve log messages

* handle service status failures and install service when needed

* removing unused files

* update documentation and description

* add version command

* update service lib version

* using lib constant for not installed services

* match version from goreleaser

* fix: graceful shutdown

* stop only if service is running

* add logs initialization to service controller commands

Co-authored-by: braginini <bangvalo@gmail.com>
2021-10-17 21:34:07 +02:00
Mikhail Bragin
96799a25b5 docs: fix gif size 2021-10-16 16:54:37 +02:00
Mikhail Bragin
07291cdb93 docs: update readme (#132)
* update readme
2021-10-16 16:53:39 +02:00
Mikhail Bragin
21139938c1 docs: highlight Slack channel 2021-10-12 14:37:49 +02:00
Maycon Santos
5cf2d0a6a9 add slack invitation link (#129) 2021-10-12 12:16:09 +02:00
Maycon Santos
8551afe04e enhancement: Support new architectures and auto upload packages to repo (#128)
* adding uploads

* adding uploads

* adding uploads

* adding uploads

* adding uploads

* adding uploads

* use https://pkgs.wiretrustee.com/

* use https://pkgs.wiretrustee.com/

* use https://pkgs.wiretrustee.com/

* set yum id

* secrets for goreleaser uploads

* ensure Github release is enabled
2021-10-12 12:15:45 +02:00
braginini
1685817171 docs: correct installation steps 2021-10-03 18:55:47 +02:00
Mikhail Bragin
e17f662683 docs: update intro (#125)
* docs: update intro
2021-10-03 18:21:41 +02:00
Mikhail Bragin
a764fb870c docs: move intro link up in readme 2021-09-27 09:23:19 +02:00
Mikhail Bragin
cabff941ac docs: add self-hosting video 2021-09-26 16:49:59 +02:00
Mikhail Bragin
b5f35dfb5e docs: replace beta with app.wiretrustee.com (#123)
* docs: replace beta with app.wiretrustee.com

* docs: add Signal port to the list of the open ports

* docs: minor corrections
2021-09-26 11:44:34 +02:00
braginini
1d426b7f81 docs: fix docker-compose management image 2021-09-25 20:17:01 +02:00
Maycon Santos
e4f9406d44 Removed installer and add workflow dispatch (#120) 2021-09-25 19:30:12 +02:00
braginini
7c79ff62ee fix: coturn port 2021-09-25 19:29:43 +02:00
Mikhail Bragin
32c369257b management/support cert from file (#122)
* feature: support cert file in management service

* docs: add new management commands
2021-09-25 19:22:49 +02:00
Mikhail Bragin
08dd719aa1 self-hosting guide (#121)
* docs: first steps of the self-hosting guide

* feature: add setup configurator for the self-hosted guide

* docs: add setup.env comments

* docs: simplify installation steps - support ./configure.sh

* docs: fix file references

* docs: fix minor docs issues

* docs: remove unused title
2021-09-25 19:12:05 +02:00
Mikhail Bragin
84c714dd93 Update quickstart.md 2021-09-23 14:39:55 +02:00
Mikhail Bragin
996c8d7c62 docs: referer to the new video 2021-09-23 14:38:51 +02:00
Mikhail Bragin
25e68ce493 docs: fix broken intro link 2021-09-22 14:18:48 +02:00
Mikhail Bragin
4881dcbd51 docs: add Getting Started hosted version guide (#119)
* docs: add Getting Started hosted version guide

* docs: fix screenshot sizes

* docs: self-hosting section

* docs: increase screenshots width

* docs: reference getting started from main readme

* docs: add refs to sections

* docs: move docs to a separate folder

* docs: add intro

* docs: correct intro docs

* docs: correct image location

* docs: correct language
2021-09-22 14:16:46 +02:00
Mikhail Bragin
d505f70972 Update README.md 2021-09-13 08:50:15 +02:00
Mikhail Bragin
6a80684378 docs: add slack 2021-09-13 08:18:18 +02:00
Mikhail Bragin
2624a7c4e6 docs: update Auth0 notes 2021-09-13 08:06:28 +02:00
Mikhail Bragin
9a412e7bf1 Update README.md 2021-09-13 07:58:52 +02:00
Mikhail Bragin
b5d1690129 Update README.md 2021-09-12 20:38:26 +02:00
Mikhail Bragin
d4bec15ca3 Update README.md 2021-09-12 20:37:55 +02:00
Mikhail Bragin
3212aca7c7 docs: add reference to auth0 react guide 2021-09-12 09:39:03 +03:00
Mikhail Bragin
b97a2251d3 fix docker compose signal volume 2021-09-12 09:08:55 +03:00
72 changed files with 2772 additions and 902 deletions

View File

@@ -8,7 +8,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.16.x]
go-version: [1.17.x]
runs-on: ubuntu-latest
steps:
- name: Install Go
@@ -24,7 +24,7 @@ jobs:
strategy:
matrix:
os: [ windows, linux, darwin ]
go-version: [1.16.x]
go-version: [1.17.x]
runs-on: ubuntu-latest
steps:
- name: Checkout code

View File

@@ -18,7 +18,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
go-version: 1.17
-
name: Cache Go modules
uses: actions/cache@v1
@@ -51,28 +51,15 @@ jobs:
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 }}
-
id: get_version
uses: battila7/get-version-action@v2
-
name: Install makensis
run: sudo apt update && sudo apt install -y nsis nsis-pluginapi
-
name: Download EnvVar Plugin
run: curl -L -o EnVar_plugin.zip https://nsis.sourceforge.io/mediawiki/images/7/7f/EnVar_plugin.zip
-
name: Extract EnVar plugin
run: sudo 7z x -o"/usr/share/nsis/" EnVar_plugin.zip
-
name: Generate Windows installer
run: makensis -V4 client/installer.nsis
env:
APPVER: ${{ steps.get_version.outputs.major }}.${{ steps.get_version.outputs.minor }}.${{ steps.get_version.outputs.patch }}.${{ github.run_id }}
-
name: Upload windows installer to release page
uses: svenstaro/upload-release-action@v2
name: Trigger Windows binaries sign pipeline
uses: benc-uk/workflow-dispatch@v1
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: wiretrustee-installer.exe
asset_name: wiretrustee_installer_${{ steps.get_version.outputs.version-without-v }}_windows_amd64.exe
tag: ${{ github.ref }}
workflow: Sign windows bin and installer
repo: wiretrustee/windows-sign-pipeline
ref: v0.0.1
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref }}" }'

4
.gitignore vendored
View File

@@ -3,4 +3,6 @@
dist/
.env
conf.json
http-cmds.sh
http-cmds.sh
infrastructure_files/management.json
infrastructure_files/docker-compose.yml

View File

@@ -13,13 +13,18 @@ builds:
- arm
- amd64
- arm64
- mips
gomips:
- hardfloat
- softfloat
ignore:
- goos: darwin
goarch: arm64
- goos: windows
goarch: arm64
- goos: windows
goarch: arm
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
mod_timestamp: '{{ .CommitTimestamp }}'
tags:
- load_wintun_from_rsrc
@@ -32,6 +37,10 @@ builds:
goarch:
- amd64
- arm64
- arm
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
mod_timestamp: '{{ .CommitTimestamp }}'
- id: wiretrustee-signal
dir: signal
@@ -42,29 +51,86 @@ builds:
goarch:
- amd64
- arm64
- arm
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
mod_timestamp: '{{ .CommitTimestamp }}'
archives:
- builds:
- wiretrustee
nfpms:
- maintainer: Wiretrustee <wiretrustee@wiretrustee.com>
description: Wiretrustee project.
- maintainer: Wiretrustee <dev@wiretrustee.com>
description: Wiretrustee client.
homepage: https://wiretrustee.com/
id: deb
builds:
- wiretrustee
formats:
- deb
- rpm
contents:
- src: release_files/wiretrustee.service
dst: /lib/systemd/system/wiretrustee.service
- src: release_files/wiretrustee.json
dst: /etc/wiretrustee/wiretrustee.json
type: "config|noreplace"
scripts:
postinstall: "release_files/post_install.sh"
preremove: "release_files/pre_remove.sh"
- maintainer: Wiretrustee <dev@wiretrustee.com>
description: Wiretrustee client.
homepage: https://wiretrustee.com/
id: rpm
builds:
- wiretrustee
formats:
- rpm
scripts:
postinstall: "release_files/post_install.sh"
preremove: "release_files/pre_remove.sh"
dockers:
- image_templates:
- wiretrustee/wiretrustee:{{ .Version }}-amd64
ids:
- wiretrustee
goarch: amd64
use: buildx
dockerfile: client/Dockerfile
build_flag_templates:
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- wiretrustee/wiretrustee:{{ .Version }}-arm64v8
ids:
- wiretrustee
goarch: arm64
use: buildx
dockerfile: client/Dockerfile
build_flag_templates:
- "--platform=linux/arm64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- wiretrustee/wiretrustee:{{ .Version }}-arm
ids:
- wiretrustee
goarch: arm
goarm: 6
use: buildx
dockerfile: client/Dockerfile
build_flag_templates:
- "--platform=linux/arm"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- wiretrustee/signal:{{ .Version }}-amd64
ids:
@@ -95,6 +161,22 @@ dockers:
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- wiretrustee/signal:{{ .Version }}-arm
ids:
- wiretrustee-signal
goarch: arm
goarm: 6
use: buildx
dockerfile: signal/Dockerfile
build_flag_templates:
- "--platform=linux/arm"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- wiretrustee/management:{{ .Version }}-amd64
ids:
@@ -125,6 +207,22 @@ dockers:
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- wiretrustee/management:{{ .Version }}-arm
ids:
- wiretrustee-mgmt
goarch: arm
goarm: 6
use: buildx
dockerfile: management/Dockerfile
build_flag_templates:
- "--platform=linux/arm"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- wiretrustee/management:{{ .Version }}-debug-amd64
ids:
@@ -156,30 +254,63 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- image_templates:
- wiretrustee/management:{{ .Version }}-debug-arm
ids:
- wiretrustee-mgmt
goarch: arm
goarm: 6
use: buildx
dockerfile: management/Dockerfile.debug
build_flag_templates:
- "--platform=linux/arm"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=wiretrustee@wiretrustee.com"
docker_manifests:
- name_template: wiretrustee/wiretrustee:{{ .Version }}
image_templates:
- wiretrustee/wiretrustee:{{ .Version }}-arm64v8
- wiretrustee/wiretrustee:{{ .Version }}-arm
- wiretrustee/wiretrustee:{{ .Version }}-amd64
- name_template: wiretrustee/wiretrustee:latest
image_templates:
- wiretrustee/wiretrustee:{{ .Version }}-arm64v8
- wiretrustee/wiretrustee:{{ .Version }}-arm
- wiretrustee/wiretrustee:{{ .Version }}-amd64
- name_template: wiretrustee/signal:{{ .Version }}
image_templates:
- wiretrustee/signal:{{ .Version }}-arm64v8
- wiretrustee/signal:{{ .Version }}-arm
- wiretrustee/signal:{{ .Version }}-amd64
- name_template: wiretrustee/signal:latest
image_templates:
- wiretrustee/signal:{{ .Version }}-arm64v8
- wiretrustee/signal:{{ .Version }}-arm
- wiretrustee/signal:{{ .Version }}-amd64
- name_template: wiretrustee/management:{{ .Version }}
image_templates:
- wiretrustee/management:{{ .Version }}-arm64v8
- wiretrustee/management:{{ .Version }}-arm
- wiretrustee/management:{{ .Version }}-amd64
- name_template: wiretrustee/management:latest
image_templates:
- wiretrustee/management:{{ .Version }}-arm64v8
- wiretrustee/management:{{ .Version }}-arm
- wiretrustee/management:{{ .Version }}-amd64
- name_template: wiretrustee/management:debug-latest
image_templates:
- wiretrustee/management:{{ .Version }}-debug-arm64v8
- wiretrustee/management:{{ .Version }}-debug-arm
- wiretrustee/management:{{ .Version }}-debug-amd64
brews:
@@ -196,4 +327,20 @@ brews:
homepage: https://wiretrustee.com/
license: "BSD3"
test: |
system "#{bin}/{{ .ProjectName }} -h"
system "#{bin}/{{ .ProjectName }} -h"
uploads:
- name: debian
ids:
- 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:
- rpm
mode: archive
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
username: dev@wiretrustee.com
method: PUT

240
README.md
View File

@@ -1,42 +1,72 @@
# Wiretrustee
<div align="center">
A WireGuard®-based mesh network that connects your devices into a single private network.
<p align="center">
<img width="250" src="docs/media/logo-full.png"/>
</p>
<p>
<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 Wiretrustee at <a href="https://app.wiretrustee.com/">app.wiretrustee.com</a>
<br/>
See <a href="https://docs.wiretrustee.com">Documentation</a>
<br/>
Join our <a href="https://join.slack.com/t/wiretrustee/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">Slack channel</a>
<br/>
</strong>
</p>
<br>
**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.
**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="docs/media/peerA.gif" width="400"/>
<img src="docs/media/peerB.gif" width="400"/>
</p>
**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).
**Hosted demo version:** [https://beta.wiretrustee.com/](https://beta.wiretrustee.com/peers)
Please don't use the hosted demonstration version for production purposes.
Hosted demo version:
[https://app.wiretrustee.com/](https://app.wiretrustee.com/peers).
[UI Dashboard Repo](https://github.com/wiretrustee/wiretrustee-dashboard)
### Why using Wiretrustee?
* Connect multiple devices to each other via a secure peer-to-peer Wireguard VPN tunnel. At home, the office, or anywhere else.
* No need to open ports and expose public IPs on the device, routers etc.
* Uses Kernel Wireguard module if available.
* Automatic network change detection. When a new peer joins the network others are notified and keys are exchanged automatically.
* Automatically reconnects in case of network failures or switches.
* Automatic NAT traversal.
* Relay server fallback in case of an unsuccessful peer-to-peer connection.
* Private key never leaves your device.
* Automatic IP address management.
* Intuitive UI Dashboard.
* Works on ARM devices (e.g. Raspberry Pi).
* Open-source (including Management Service)
### Secure peer-to-peer VPN in minutes
![animation](media/peers.gif)
### A bit on Wiretrustee internals
* Wiretrustee features a Management Service that offers peer IP management and network updates distribution (e.g. when new peer joins the network).
* 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 relay server (TURN), and a secure Wireguard tunnel is established via TURN server.
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in Wiretrustee setups.
* 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.
<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)
@@ -44,116 +74,124 @@ Please don't use the hosted demonstration version for production purposes.
### Client Installation
#### Linux
1. Checkout Wiretrustee [releases](https://github.com/wiretrustee/wiretrustee/releases)
2. Download the latest release (**Switch VERSION to the latest**):
**Debian packages**
```shell
wget https://github.com/wiretrustee/wiretrustee/releases/download/v<VERSION>/wiretrustee_<VERSION>_linux_amd64.deb
```
3. Install the package
```shell
sudo dpkg -i wiretrustee_<VERSION>_linux_amd64.deb
```
**Fedora/Centos packages**
```shell
wget https://github.com/wiretrustee/wiretrustee/releases/download/v<VERSION>/wiretrustee_<VERSION>_linux_amd64.rpm
```
3. Install the package
```shell
sudo rpm -i wiretrustee_<VERSION>_linux_amd64.rpm
```
**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
```
```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
```
```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
```
```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
````
````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.
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
sudo wiretrustee login --setup-key <SETUP KEY>
docker run --network host --privileged --rm -d -e WT_SETUP_KEY=<SETUP KEY> -v wiretrustee-client:/etc/wiretrustee wiretrustee/wiretrustee:<TAG>
```
For **Windows** systems:
```shell
.\wiretrustee.exe login --setup-key <SETUP KEY>
```
> 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 login --setup-key <SETUP KEY> --management-url https://localhost:33073
```
```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.
> You could also omit `--setup-key` property. In this case the tool will prompt it the key.
2. Start Wiretrustee:
For **MACOS** you will just start the service:
````shell
sudo wiretrustee up
# or
sudo wiretrustee up & # to run it in background
````
2. Check your IP:
For **MACOS** you will just start the service:
````shell
sudo ipconfig getifaddr utun100
````
For **Linux** systems:
```shell
sudo systemctl restart wiretrustee.service
sudo systemctl status wiretrustee.service
```
```shell
ip addr show wt0
```
For **Windows** systems:
```shell
.\wiretrustee.exe service start
```
> You may need to run Powershell as Administrator
```shell
netsh interface ip show config name="wt0"
```
3. 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.
4. Repeat on other machines.
### Running Management, Signal and Coturn
### 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 management).
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.
Under infrastructure_files we have a docker-compose example to run both, 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.
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).

4
client/Dockerfile Normal file
View File

@@ -0,0 +1,4 @@
FROM gcr.io/distroless/base:debug
ENV WT_LOG_FILE=console
ENTRYPOINT [ "/go/bin/wiretrustee","up"]
COPY wiretrustee /go/bin/wiretrustee

View File

@@ -18,22 +18,21 @@ import (
)
var (
setupKey string
loginCmd = &cobra.Command{
Use: "login",
Short: "login to the Wiretrustee Management Service (first run)",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
err := util.InitLog(logLevel, logFile)
if err != nil {
log.Errorf("failed initializing log %v", err)
return err
}
config, err := internal.GetConfig(managementURL, configPath)
config, err := internal.GetConfig(managementURL, configPath, preSharedKey)
if err != nil {
log.Errorf("failed getting config %s %v", configPath, err)
//os.Exit(ExitSetupFailed)
return err
}
@@ -41,7 +40,6 @@ var (
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
if err != nil {
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
//os.Exit(ExitSetupFailed)
return err
}
@@ -56,7 +54,6 @@ var (
mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
if err != nil {
log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), err)
//os.Exit(ExitSetupFailed)
return err
}
log.Debugf("connected to anagement Service %s", config.ManagementURL.String())
@@ -64,21 +61,18 @@ var (
serverKey, err := mgmClient.GetServerPublicKey()
if err != nil {
log.Errorf("failed while getting Management Service public key: %v", err)
//os.Exit(ExitSetupFailed)
return err
}
_, err = loginPeer(*serverKey, mgmClient, setupKey)
if err != nil {
log.Errorf("failed logging-in peer on Management Service : %v", err)
//os.Exit(ExitSetupFailed)
return err
}
err = mgmClient.Close()
if err != nil {
log.Errorf("failed closing Management Service client: %v", err)
//os.Exit(ExitSetupFailed)
return err
}
@@ -151,7 +145,3 @@ func promptPeerSetupKey() (string, error) {
return "", s.Err()
}
func init() {
loginCmd.PersistentFlags().StringVar(&setupKey, "setup-key", "", "Setup key obtained from the Management Service Dashboard (used to register peer)")
}

View File

@@ -2,17 +2,15 @@ package cmd
import (
"fmt"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/wiretrustee/wiretrustee/client/internal"
"os"
"os/signal"
"runtime"
)
const (
// ExitSetupFailed defines exit code
ExitSetupFailed = 1
DefaultConfigPath = ""
"strings"
"syscall"
)
var (
@@ -22,15 +20,17 @@ var (
defaultLogFile string
logFile string
managementURL string
rootCmd = &cobra.Command{
setupKey string
preSharedKey string
rootCmd = &cobra.Command{
Use: "wiretrustee",
Short: "",
Long: "",
}
// Execution control channel for stopCh signal
stopCh chan int
stopCh chan int
cleanupCh chan struct{}
)
// Execute executes the root command.
@@ -40,6 +40,7 @@ func Execute() error {
func init() {
stopCh = make(chan int)
cleanupCh = make(chan struct{})
defaultConfigPath = "/etc/wiretrustee/config.json"
defaultLogFile = "/var/log/wiretrustee/client.log"
@@ -52,9 +53,12 @@ func init() {
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(loginCmd)
rootCmd.AddCommand(versionCmd)
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
}
@@ -62,11 +66,36 @@ func init() {
// SetupCloseHandler handles SIGTERM signal and exits with success
func SetupCloseHandler() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
for range c {
fmt.Println("\r- Ctrl+C pressed in Terminal")
log.Info("shutdown signal received")
stopCh <- 0
}
}()
}
// SetFlagsFromEnvVars reads and updates flag values from environment variables with prefix WT_
func SetFlagsFromEnvVars() {
flags := rootCmd.PersistentFlags()
flags.VisitAll(func(f *pflag.Flag) {
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, 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 WT_SETUP_KEYS)
func FlagNameToEnvVar(f string) string {
prefix := "WT_"
parsed := strings.ReplaceAll(f, "-", "_")
upper := strings.ToUpper(parsed)
return prefix + upper
}

View File

@@ -34,6 +34,3 @@ var (
Short: "manages wiretrustee service",
}
)
func init() {
}

View File

@@ -4,23 +4,35 @@ import (
"github.com/kardianos/service"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/wiretrustee/wiretrustee/util"
"time"
)
func (p *program) Start(s service.Service) error {
func (p *program) Start(service.Service) error {
// Start should not block. Do the actual work async.
log.Info("starting service") //nolint
go func() {
err := upCmd.RunE(p.cmd, p.args)
err := runClient()
if err != nil {
log.Errorf("stopped Wiretrustee client app due to error: %v", err)
return
}
}()
return nil
}
func (p *program) Stop(s service.Service) error {
stopCh <- 1
func (p *program) Stop(service.Service) error {
go func() {
stopCh <- 1
}()
select {
case <-cleanupCh:
case <-time.After(time.Second * 10):
log.Warnf("failed waiting for service cleanup, terminating")
}
log.Info("stopped Wiretrustee service") //nolint
return nil
}
@@ -29,6 +41,15 @@ var (
Use: "run",
Short: "runs wiretrustee as service",
Run: func(cmd *cobra.Command, args []string) {
SetFlagsFromEnvVars()
err := util.InitLog(logLevel, logFile)
if err != nil {
log.Errorf("failed initializing log %v", err)
return
}
SetupCloseHandler()
prg := &program{
cmd: cmd,
@@ -54,19 +75,26 @@ var (
startCmd = &cobra.Command{
Use: "start",
Short: "starts wiretrustee service",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
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
return err
}
err = s.Start()
if err != nil {
cmd.PrintErrln(err)
return
return err
}
cmd.Printf("Wiretrustee service has been started")
cmd.Println("Wiretrustee service has been started")
return nil
},
}
)
@@ -76,7 +104,12 @@ var (
Use: "stop",
Short: "stops wiretrustee service",
Run: func(cmd *cobra.Command, args []string) {
SetFlagsFromEnvVars()
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)
@@ -87,7 +120,7 @@ var (
cmd.PrintErrln(err)
return
}
cmd.Printf("Wiretrustee service has been stopped")
cmd.Println("Wiretrustee service has been stopped")
},
}
)
@@ -97,7 +130,12 @@ var (
Use: "restart",
Short: "restarts wiretrustee service",
Run: func(cmd *cobra.Command, args []string) {
SetFlagsFromEnvVars()
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)
@@ -108,10 +146,7 @@ var (
cmd.PrintErrln(err)
return
}
cmd.Printf("Wiretrustee service has been restarted")
cmd.Println("Wiretrustee service has been restarted")
},
}
)
func init() {
}

View File

@@ -9,7 +9,8 @@ var (
installCmd = &cobra.Command{
Use: "install",
Short: "installs wiretrustee service",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
svcConfig := newSVCConfig()
@@ -30,15 +31,16 @@ var (
s, err := newSVC(&program{}, svcConfig)
if err != nil {
cmd.PrintErrln(err)
return
return err
}
err = s.Install()
if err != nil {
cmd.PrintErrln(err)
return
return err
}
cmd.Printf("Wiretrustee service has been installed")
cmd.Println("Wiretrustee service has been installed")
return nil
},
}
)
@@ -48,6 +50,7 @@ var (
Use: "uninstall",
Short: "uninstalls wiretrustee service from system",
Run: func(cmd *cobra.Command, args []string) {
SetFlagsFromEnvVars()
s, err := newSVC(&program{}, newSVCConfig())
if err != nil {
@@ -60,10 +63,7 @@ var (
cmd.PrintErrln(err)
return
}
cmd.Printf("Wiretrustee has been uninstalled")
cmd.Println("Wiretrustee has been uninstalled")
},
}
)
func init() {
}

View File

@@ -2,123 +2,83 @@ package cmd
import (
"context"
"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"
"github.com/wiretrustee/wiretrustee/util"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"time"
)
var (
upCmd = &cobra.Command{
Use: "up",
Short: "start wiretrustee",
Short: "install, login and start wiretrustee client",
RunE: func(cmd *cobra.Command, args []string) error {
err := util.InitLog(logLevel, logFile)
SetFlagsFromEnvVars()
err := loginCmd.RunE(cmd, args)
if err != nil {
log.Errorf("failed initializing log %v", err)
return err
}
if logFile == "console" {
return runClient()
}
s, err := newSVC(&program{}, newSVCConfig())
if err != nil {
cmd.PrintErrln(err)
return err
}
config, err := internal.ReadConfig(managementURL, configPath)
srvStatus, err := s.Status()
if err != nil {
log.Errorf("failed reading config %s %v", configPath, err)
return err
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)
}
}
//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
if srvStatus == service.StatusRunning {
stopCmd.Run(cmd, args)
}
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
}
engineConfig, err := createEngineConfig(myPrivateKey, config, loginResp.GetWiretrusteeConfig(), loginResp.GetPeerConfig())
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)
err = engine.Start()
if err != nil {
log.Errorf("error while starting Wiretrustee Connection Engine: %s", err)
return err
}
SetupCloseHandler()
select {
case <-stopCh:
case <-ctx.Done():
}
log.Infof("receive signal to stop running")
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
}
return nil
return startCmd.RunE(cmd, args)
},
}
)
func init() {
}
// createEngineConfig converts configuration received from Management Service to EngineConfig
func createEngineConfig(key wgtypes.Key, config *internal.Config, wtConfig *mgmProto.WiretrusteeConfig, peerConfig *mgmProto.PeerConfig) (*internal.EngineConfig, error) {
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{}{}
}
return &internal.EngineConfig{
engineConf := &internal.EngineConfig{
WgIface: config.WgIface,
WgAddr: peerConfig.Address,
IFaceBlackList: iFaceBlackList,
WgPrivateKey: key,
}, nil
}
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
@@ -163,7 +123,113 @@ func connectToManagement(ctx context.Context, managementAddr string, ourPrivateK
}
}
log.Infof("peer logged in to Management Service %s", managementAddr)
log.Debugf("peer logged in to Management Service %s", managementAddr)
return client, loginResp, nil
}
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,
}
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,13 +1,10 @@
package cmd
import (
"errors"
"fmt"
"github.com/wiretrustee/wiretrustee/iface"
mgmt "github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/util"
"net/url"
"os"
"path/filepath"
"testing"
"time"
@@ -37,24 +34,6 @@ func TestUp_Start(t *testing.T) {
}
func TestUp_ShouldFail_On_NoConfig(t *testing.T) {
tempDir := t.TempDir()
confPath := tempDir + "/config.json"
mgmtURL := fmt.Sprintf("http://%s", mgmAddr)
rootCmd.SetArgs([]string{
"up",
"--config",
confPath,
"--management-url",
mgmtURL,
})
err := rootCmd.Execute()
if err == nil || !errors.Is(err, os.ErrNotExist) {
t.Errorf("expecting login command to fail on absence of config")
}
}
func TestUp(t *testing.T) {
defer iface.Close()
@@ -65,24 +44,17 @@ func TestUp(t *testing.T) {
if err != nil {
t.Fatal(err)
}
rootCmd.SetArgs([]string{
"login",
"up",
"--config",
confPath,
"--setup-key",
"A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
"--management-url",
mgmtURL.String(),
})
err = rootCmd.Execute()
if err != nil {
t.Fatal(err)
}
rootCmd.SetArgs([]string{
"up",
"--config",
confPath,
"--log-file",
"console",
})
go func() {
err = rootCmd.Execute()

14
client/cmd/version.go Normal file
View File

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

View File

@@ -106,6 +106,7 @@ SectionEnd
Section Uninstall
${INSTALL_TYPE}
Exec '"$INSTDIR\${MAIN_APP_EXE}" service stop'
Exec '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
# wait the service uninstall take unblock the executable
Sleep 3000

View File

@@ -28,13 +28,14 @@ func init() {
type Config struct {
// Wireguard private key of local peer
PrivateKey string
PreSharedKey string
ManagementURL *url.URL
WgIface string
IFaceBlackList []string
}
//createNewConfig creates a new config generating a new Wireguard key and saving to file
func createNewConfig(managementURL string, configPath string) (*Config, error) {
func createNewConfig(managementURL string, configPath string, preSharedKey string) (*Config, error) {
wgKey := generateKey()
config := &Config{PrivateKey: wgKey, WgIface: iface.WgInterfaceDefault, IFaceBlackList: []string{}}
if managementURL != "" {
@@ -47,6 +48,10 @@ func createNewConfig(managementURL string, configPath string) (*Config, error) {
config.ManagementURL = managementURLDefault
}
if preSharedKey != "" {
config.PreSharedKey = preSharedKey
}
config.IFaceBlackList = []string{iface.WgInterfaceDefault, "tun0"}
err := util.WriteJson(configPath, config)
@@ -93,11 +98,11 @@ func ReadConfig(managementURL string, configPath string) (*Config, error) {
}
// GetConfig reads existing config or generates a new one
func GetConfig(managementURL string, configPath string) (*Config, error) {
func GetConfig(managementURL string, configPath string, preSharedKey string) (*Config, error) {
if _, err := os.Stat(configPath); os.IsNotExist(err) {
log.Infof("generating new config %s", configPath)
return createNewConfig(managementURL, configPath)
return createNewConfig(managementURL, configPath, preSharedKey)
} else {
return ReadConfig(managementURL, configPath)
}

View File

@@ -60,6 +60,8 @@ type ConnConfig struct {
// Remote Wireguard public key
RemoteWgKey wgtypes.Key
PreSharedKey *wgtypes.Key
StunTurnURLS []*ice.URL
iFaceBlackList map[string]struct{}
@@ -115,7 +117,7 @@ func NewConnection(config ConnConfig,
closeCond: NewCond(),
connected: NewCond(),
agent: nil,
wgProxy: NewWgProxy(config.WgIface, config.RemoteWgKey.String(), config.WgAllowedIPs, config.WgListenAddr),
wgProxy: NewWgProxy(config.WgIface, config.RemoteWgKey.String(), config.WgAllowedIPs, config.WgListenAddr, config.PreSharedKey),
Status: StatusDisconnected,
}
}
@@ -138,12 +140,18 @@ func (conn *Connection) Open(timeout time.Duration) error {
return !ok
},
})
conn.agent = a
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
@@ -160,13 +168,13 @@ func (conn *Connection) Open(timeout time.Duration) error {
}
conn.Status = StatusConnecting
log.Infof("trying to connect to peer %s", conn.Config.RemoteWgKey.String())
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.Infof("got a connection confirmation from peer %s", conn.Config.RemoteWgKey.String())
log.Debugf("got a connection confirmation from peer %s", conn.Config.RemoteWgKey.String())
err = conn.agent.GatherCandidates()
if err != nil {
@@ -186,8 +194,11 @@ func (conn *Connection) Open(timeout time.Duration) error {
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(pair) {
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 {
@@ -195,19 +206,17 @@ func (conn *Connection) Open(timeout time.Duration) error {
}
} else {
log.Infof("establishing secure tunnel to peer %s via selected candidate pair %s", conn.Config.RemoteWgKey.String(), pair)
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
}
}
if pair.Remote.Type() == ice.CandidateTypeRelay || pair.Local.Type() == ice.CandidateTypeRelay {
log.Infof("using relay with peer %s", conn.Config.RemoteWgKey)
}
relayed := pair.Remote.Type() == ice.CandidateTypeRelay || pair.Local.Type() == ice.CandidateTypeRelay
conn.Status = StatusConnected
log.Infof("opened connection to peer %s", conn.Config.RemoteWgKey.String())
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())
@@ -271,7 +280,7 @@ func (conn *Connection) Close() error {
var err error
conn.closeCond.Do(func() {
log.Warnf("closing connection to peer %s", conn.Config.RemoteWgKey.String())
log.Debugf("closing connection to peer %s", conn.Config.RemoteWgKey.String())
if a := conn.agent; a != nil {
e := a.Close()

View File

@@ -4,7 +4,7 @@ import (
"context"
"fmt"
"github.com/cenkalti/backoff/v4"
ice "github.com/pion/ice/v2"
"github.com/pion/ice/v2"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/iface"
mgm "github.com/wiretrustee/wiretrustee/management/client"
@@ -30,6 +30,8 @@ type EngineConfig struct {
WgPrivateKey wgtypes.Key
// IFaceBlackList is a list of network interfaces to ignore when discovering connection candidates (ICE related)
IFaceBlackList map[string]struct{}
PreSharedKey *wgtypes.Key
}
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
@@ -57,6 +59,8 @@ type Engine struct {
TURNs []*ice.URL
cancel context.CancelFunc
ctx context.Context
}
// Peer is an instance of the Connection Peer
@@ -66,7 +70,7 @@ type Peer struct {
}
// NewEngine creates a new Connection Engine
func NewEngine(signalClient *signal.Client, mgmClient *mgm.Client, config *EngineConfig, cancel context.CancelFunc) *Engine {
func NewEngine(signalClient *signal.Client, mgmClient *mgm.Client, config *EngineConfig, cancel context.CancelFunc, ctx context.Context) *Engine {
return &Engine{
signal: signalClient,
mgmClient: mgmClient,
@@ -77,17 +81,25 @@ func NewEngine(signalClient *signal.Client, mgmClient *mgm.Client, config *Engin
STUNs: []*ice.URL{},
TURNs: []*ice.URL{},
cancel: cancel,
ctx: ctx,
}
}
func (e *Engine) Stop() error {
err := e.removeAllPeerConnections()
if err != nil {
return err
}
log.Debugf("removing Wiretrustee interface %s", e.config.WgIface)
err := iface.Close()
err = iface.Close()
if err != nil {
log.Errorf("failed closing Wiretrustee interface %s %v", e.config.WgIface, err)
return err
}
log.Infof("stopped Wiretrustee Engine")
return nil
}
@@ -127,27 +139,32 @@ func (e *Engine) Start() error {
// initializePeer peer agent attempt to open connection
func (e *Engine) initializePeer(peer Peer) {
var backOff = &backoff.ExponentialBackOff{
var backOff = backoff.WithContext(&backoff.ExponentialBackOff{
InitialInterval: backoff.DefaultInitialInterval,
RandomizationFactor: backoff.DefaultRandomizationFactor,
Multiplier: backoff.DefaultMultiplier,
MaxInterval: 5 * time.Second,
MaxElapsedTime: time.Duration(0), //never stop
MaxElapsedTime: 0, //never stop
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}
}, e.ctx)
operation := func() error {
if e.signal.GetStatus() != signal.StreamConnected {
return fmt.Errorf("not opening connection to peer because Signal is unavailable")
}
_, err := e.openPeerConnection(e.wgPort, e.config.WgPrivateKey, peer)
e.peerMux.Lock()
defer e.peerMux.Unlock()
if _, ok := e.conns[peer.WgPubKey]; !ok {
log.Infof("removing connection attempt with Peer: %v, not retrying", peer.WgPubKey)
log.Debugf("removed connection attempt to peer: %v, not retrying", peer.WgPubKey)
return nil
}
if err != nil {
log.Warnln(err)
log.Warnln("retrying connection because of error: ", err.Error())
log.Debugf("retrying connection because of error: %s", err.Error())
return err
}
return nil
@@ -172,6 +189,19 @@ func (e *Engine) removePeerConnections(peers []string) error {
return nil
}
func (e *Engine) removeAllPeerConnections() error {
log.Debugf("removing all peer connections")
e.peerMux.Lock()
defer e.peerMux.Unlock()
for peer := range e.conns {
err := e.removePeerConnection(peer)
if err != nil {
return err
}
}
return nil
}
// removePeerConnection closes existing peer connection and removes peer
func (e *Engine) removePeerConnection(peerKey string) error {
conn, exists := e.conns[peerKey]
@@ -179,6 +209,7 @@ func (e *Engine) removePeerConnection(peerKey string) error {
delete(e.conns, peerKey)
return conn.Close()
}
log.Infof("removed connection to peer %s", peerKey)
return nil
}
@@ -209,6 +240,7 @@ func (e *Engine) openPeerConnection(wgPort int, myKey wgtypes.Key, peer Peer) (*
RemoteWgKey: remoteKey,
StunTurnURLS: append(e.STUNs, e.TURNs...),
iFaceBlackList: e.config.IFaceBlackList,
PreSharedKey: e.config.PreSharedKey,
}
signalOffer := func(uFrag string, pwd string) error {
@@ -307,10 +339,12 @@ func (e *Engine) receiveManagementEvents() {
return nil
})
if err != nil {
// happens if management is unavailable for a long time.
// We want to cancel the operation of the whole client
e.cancel()
return
}
log.Infof("connected to Management Service updates stream")
log.Debugf("stopped receiving updates from Management Service")
}()
log.Debugf("connecting to Management Service updates stream")
}
@@ -389,68 +423,77 @@ func (e *Engine) updatePeers(remotePeers []*mgmProto.RemotePeerConfig) error {
// receiveSignalEvents connects to the Signal Service event stream to negotiate connection with remote peers
func (e *Engine) receiveSignalEvents() {
// connect to a stream of messages coming from the signal server
e.signal.Receive(func(msg *sProto.Message) error {
e.syncMsgMux.Lock()
defer e.syncMsgMux.Unlock()
go func() {
// connect to a stream of messages coming from the signal server
err := e.signal.Receive(func(msg *sProto.Message) error {
conn := e.conns[msg.Key]
if conn == nil {
return fmt.Errorf("wrongly addressed message %s", msg.Key)
}
e.syncMsgMux.Lock()
defer e.syncMsgMux.Unlock()
if conn.Config.RemoteWgKey.String() != msg.Key {
return fmt.Errorf("unknown peer %s", msg.Key)
}
switch msg.GetBody().Type {
case sProto.Body_OFFER:
remoteCred, err := signal.UnMarshalCredential(msg)
if err != nil {
return err
conn := e.conns[msg.Key]
if conn == nil {
return fmt.Errorf("wrongly addressed message %s", msg.Key)
}
err = conn.OnOffer(IceCredentials{
uFrag: remoteCred.UFrag,
pwd: remoteCred.Pwd,
})
if err != nil {
return err
if conn.Config.RemoteWgKey.String() != msg.Key {
return fmt.Errorf("unknown peer %s", msg.Key)
}
switch msg.GetBody().Type {
case sProto.Body_OFFER:
remoteCred, err := signal.UnMarshalCredential(msg)
if err != nil {
return err
}
err = conn.OnOffer(IceCredentials{
uFrag: remoteCred.UFrag,
pwd: remoteCred.Pwd,
})
if err != nil {
return err
}
return nil
case sProto.Body_ANSWER:
remoteCred, err := signal.UnMarshalCredential(msg)
if err != nil {
return err
}
err = conn.OnAnswer(IceCredentials{
uFrag: remoteCred.UFrag,
pwd: remoteCred.Pwd,
})
if err != nil {
return err
}
case sProto.Body_CANDIDATE:
candidate, err := ice.UnmarshalCandidate(msg.GetBody().Payload)
if err != nil {
log.Errorf("failed on parsing remote candidate %s -> %s", candidate, err)
return err
}
err = conn.OnRemoteCandidate(candidate)
if err != nil {
log.Errorf("error handling CANDIATE from %s", msg.Key)
return err
}
}
return nil
case sProto.Body_ANSWER:
remoteCred, err := signal.UnMarshalCredential(msg)
if err != nil {
return err
}
err = conn.OnAnswer(IceCredentials{
uFrag: remoteCred.UFrag,
pwd: remoteCred.Pwd,
})
if err != nil {
return err
}
case sProto.Body_CANDIDATE:
candidate, err := ice.UnmarshalCandidate(msg.GetBody().Payload)
if err != nil {
log.Errorf("failed on parsing remote candidate %s -> %s", candidate, err)
return err
}
err = conn.OnRemoteCandidate(candidate)
if err != nil {
log.Errorf("error handling CANDIATE from %s", msg.Key)
return err
}
})
if err != nil {
// happens if signal is unavailable for a long time.
// We want to cancel the operation of the whole client
e.cancel()
return
}
}()
return nil
})
e.signal.WaitConnected()
e.signal.WaitStreamConnected()
}

View File

@@ -4,27 +4,30 @@ import (
ice "github.com/pion/ice/v2"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/iface"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"net"
)
// WgProxy an instance of an instance of the Connection Wireguard Proxy
type WgProxy struct {
iface string
remoteKey string
allowedIps string
wgAddr string
close chan struct{}
wgConn net.Conn
iface string
remoteKey string
allowedIps string
wgAddr string
close chan struct{}
wgConn net.Conn
preSharedKey *wgtypes.Key
}
// NewWgProxy creates a new Connection Wireguard Proxy
func NewWgProxy(iface string, remoteKey string, allowedIps string, wgAddr string) *WgProxy {
func NewWgProxy(iface string, remoteKey string, allowedIps string, wgAddr string, preSharedKey *wgtypes.Key) *WgProxy {
return &WgProxy{
iface: iface,
remoteKey: remoteKey,
allowedIps: allowedIps,
wgAddr: wgAddr,
close: make(chan struct{}),
iface: iface,
remoteKey: remoteKey,
allowedIps: allowedIps,
wgAddr: wgAddr,
close: make(chan struct{}),
preSharedKey: preSharedKey,
}
}
@@ -48,7 +51,7 @@ func (p *WgProxy) Close() error {
// StartLocal configure the interface with a peer using a direct IP:Port endpoint to the remote host
func (p *WgProxy) StartLocal(host string) error {
err := iface.UpdatePeer(p.iface, p.remoteKey, p.allowedIps, DefaultWgKeepAlive, host)
err := iface.UpdatePeer(p.iface, p.remoteKey, p.allowedIps, DefaultWgKeepAlive, host, p.preSharedKey)
if err != nil {
log.Errorf("error while configuring Wireguard peer [%s] %s", p.remoteKey, err.Error())
return err
@@ -67,7 +70,7 @@ func (p *WgProxy) Start(remoteConn *ice.Conn) error {
p.wgConn = wgConn
// add local proxy connection as a Wireguard peer
err = iface.UpdatePeer(p.iface, p.remoteKey, p.allowedIps, DefaultWgKeepAlive,
wgConn.LocalAddr().String())
wgConn.LocalAddr().String(), p.preSharedKey)
if err != nil {
log.Errorf("error while configuring Wireguard peer [%s] %s", p.remoteKey, err.Error())
return err
@@ -87,18 +90,16 @@ func (p *WgProxy) proxyToRemotePeer(remoteConn *ice.Conn) {
for {
select {
case <-p.close:
log.Infof("stopped proxying from remote peer %s due to closed connection", p.remoteKey)
log.Debugf("stopped proxying from remote peer %s due to closed connection", p.remoteKey)
return
default:
n, err := p.wgConn.Read(buf)
if err != nil {
//log.Warnln("failed reading from peer: ", err.Error())
continue
}
_, err = remoteConn.Write(buf[:n])
if err != nil {
//log.Warnln("failed writing to remote peer: ", err.Error())
continue
}
}
@@ -113,18 +114,16 @@ func (p *WgProxy) proxyToLocalWireguard(remoteConn *ice.Conn) {
for {
select {
case <-p.close:
log.Infof("stopped proxying from remote peer %s due to closed connection", p.remoteKey)
log.Debugf("stopped proxying from remote peer %s due to closed connection", p.remoteKey)
return
default:
n, err := remoteConn.Read(buf)
if err != nil {
//log.Errorf("failed reading from remote connection %s", err)
continue
}
_, err = p.wgConn.Write(buf[:n])
if err != nil {
//log.Errorf("failed writing to local Wireguard instance %s", err)
continue
}
}

View File

@@ -5,7 +5,11 @@ import (
"os"
)
var version = "development"
func main() {
cmd.Version = version
if err := cmd.Execute(); err != nil {
os.Exit(1)
}

104
docs/README.md Normal file
View File

@@ -0,0 +1,104 @@
### Table of contents
* [About Wiretrustee](#about-wiretrustee)
* [Why Wireguard with Wiretrustee?](#why-wireguard-with-wiretrustee)
* [Wiretrustee vs. Traditional VPN](#wiretrustee-vs-traditional-vpn)
* [High-level technology overview](#high-level-technology-overview)
* [Getting started](#getting-started)
### About Wiretrustee
Wiretrustee is an open-source VPN platform built on top of [WireGuard®](https://www.wireguard.com/) making it easy to create secure private networks for your organization or home.
It requires zero configuration effort leaving behind the hassle of opening ports, complex firewall rules, vpn gateways, and so forth.
There is no centralized VPN server with Wiretrustee - your computers, devices, machines, and servers connect to each other directly over a fast encrypted tunnel.
It literally takes less than 5 minutes to provision a secure peer-to-peer VPN with Wiretrustee. Check our [Quickstart Guide Video](https://www.youtube.com/watch?v=cWTsGUJAUaU) to see the setup in action.
### Why Wireguard with Wiretrustee?
WireGuard is a modern and extremely fast VPN tunnel utilizing state-of-the-art [cryptography](https://www.wireguard.com/protocol/)
and Wiretrustee uses Wireguard to establish a secure tunnel between machines.
Built with simplicity in mind, Wireguard ensures that traffic between two machines is encrypted and flowing, however, it requires a few things to be done beforehand.
First, in order to connect, the machines have to be configured.
On each machine, you need to generate private and public keys and prepare a WireGuard configuration file.
The configuration also includes a private IP address that should be unique per machine.
Secondly, to accept the incoming traffic, the machines have to trust each other.
The generated public keys have to be pre-shared on the machines.
This works similarly to SSH with its authorised_keys file.
Lastly, the connectivity between the machines has to be ensured.
To make machines reach one another, you are required to set a WireGuard endpoint property which indicates the IP address and port of the remote machine to connect to.
On many occasions, machines are hidden behind firewalls and NAT devices,
meaning that you may need to configure a port forwarding or open holes in your firewall to ensure the machines are reachable.
The undertakings mentioned above might not be complicated if you have just a few machines, but the complexity grows as the number of machines increases.
Wiretrustee simplifies the setup by automatically generating private and public keys, assigning unique private IP addresses, and takes care of sharing public keys between the machines.
It is worth mentioning that the private key never leaves the machine.
So only the machine that owns the key can decrypt traffic addressed to it.
The same applies also to the relayed traffic mentioned below.
Furthermore, Wiretrustee ensures connectivity by leveraging advanced [NAT traversal techniques](https://en.wikipedia.org/wiki/NAT_traversal)
and removing the necessity of port forwarding, opening holes in the firewall, and having a public static IP address.
In cases when a direct peer-to-peer connection isn't possible, all traffic is relayed securely between peers.
Wiretrustee also monitors the connection health and restarts broken connections.
There are a few more things that we are working on to make secure private networks simple. A few examples are ACLs, MFA and activity monitoring.
Check out the WireGuard [Quick Start](https://www.wireguard.com/quickstart/) guide to learn more about configuring "plain" WireGuard without Wiretrustee.
### Wiretrustee vs. Traditional VPN
In the traditional VPN model, everything converges on a centralized, protected network where all the clients are connecting to a central VPN server.
An increasing amount of connections can easily overload the VPN server.
Even a short downtime of a server can cause expensive system disruptions, and a remote team's inability to work.
Centralized VPNs imply all the traffic going through the central server causing network delays and increased traffic usage.
Such systems require an experienced team to set up and maintain.
Configuring firewalls, setting up NATs, SSO integration, and managing access control lists can be a nightmare.
Traditional centralized VPNs are often compared to a [castle-and-moat](https://en.wikipedia.org/wiki/Moat) model
in which once accessed, user is trusted and can access critical infrastructure and resources without any restrictions.
Wiretrustee decentralizes networks using direct point-to-point connections, as opposed to traditional models.
Consequently, network performance is increased since traffic flows directly between the machines bypassing VPN servers or gateways.
To achieve this, Wiretrustee client applications employ signalling servers to find other machines and negotiate connections.
These are similar to the signaling servers used in [WebRTC](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling#the_signaling_server)
Thanks to [NAT traversal techniques](https://en.wikipedia.org/wiki/NAT_traversal),
outlined in the [Why not just Wireguard?](#why-wireguard-with-wiretrustee) section above,
Wiretrustee installation doesn't require complex network and firewall configuration.
It just works, minimising the maintenance effort.
Finally, each machine or device in the Wiretrustee network verifies incoming connections accepting only the trusted ones.
This is ensured by Wireguard's [Crypto Routing concept](https://www.wireguard.com/#cryptokey-routing).
### High-level technology overview
In essence, Wiretrustee is an open source platform consisting of a collection of systems, responsible for handling peer-to-peer connections, tunneling and network management (IP, keys, ACLs, etc).
<p align="center">
<img src="media/high-level-dia.png" alt="high-level-dia" width="781"/>
</p>
Wiretrustee uses open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), [Coturn](https://github.com/coturn/coturn),
and [software](https://github.com/wiretrustee/wiretrustee) developed by Wiretrustee authors to make it all work together.
To learn more about Wiretrustee architecture, please refer to the [architecture section](../docs/architecture.md).
### Getting Started
There are 2 ways of getting started with Wiretrustee:
- use Cloud Managed version
- self-hosting
We recommend starting with the cloud managed version hosted at [app.wiretrustee.com](https://app.wiretrustee.com) - the quickest way to get familiar with the system.
See [Quickstart Guide](../docs/quickstart.md) for instructions.
If you don't want to use the managed version, check out our [Self-hosting Guide](../docs/self-hosting.md).

2
docs/architecture.md Normal file
View File

@@ -0,0 +1,2 @@
### Architecture
TODO

BIN
docs/media/add-peer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
docs/media/auth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
docs/media/empty-peers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/media/logo-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
docs/media/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/media/peerA.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

BIN
docs/media/peerB.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 MiB

After

Width:  |  Height:  |  Size: 5.9 MiB

BIN
docs/media/peers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

41
docs/quickstart.md Normal file
View File

@@ -0,0 +1,41 @@
## Quickstart guide (Cloud Managed version)
Step-by-step video guide on YouTube:
[![IMAGE ALT TEXT](https://img.youtube.com/vi/cWTsGUJAUaU/0.jpg)](https://youtu.be/cWTsGUJAUaU "Wiretrustee - secure private network in less than 5 minutes")
This guide describes how to create secure VPN and connect 2 machines peer-to-peer.
One machine is a Raspberry Pi Compute Module 4 hosted at home (Peer A), and the other one is a regular Ubuntu server running in the Data Center (Peer B).
Both machines are running Linux (Raspbian and Ubuntu respectively), but you could also use Mac or Windows operating systems.
1. Sign-up at [https://app.wiretrustee.com/](https://app.wiretrustee.com/peers)
You can use your email and password to sign-up or any available social login option (e.g., GitHub account)
<img src="media/auth.png" alt="auth" width="350"/>
2. After a successful login you will be redirected to the ```Peers``` screen which is empty because you don't have any peers yet.
Click ```Add peer``` to add a new machine.
<img src="media/empty-peers.png" alt="empty-peers" width="700"/>
3. Choose a setup key which will be used to associate your new machine with your account (in our case it is ```Default key```).
Choose your machine operating system (in our case it is ```Linux```) and proceed with the installation steps on the machine.
<img src="media/add-peer.png" alt="add-peer" width="700"/>
4. Repeat #3 for the 2nd machine.
5. Return to ```Peers``` and you should notice 2 new machines with status ```Connected```
<img src="media/peers.png" alt="peers" width="700"/>
6. To test the connection you could try pinging devices:
On Peer A:
```ping 100.64.0.2```
On Peer B:
```ping 100.64.0.1```
7. Done! You now have a secure peer-to-peer VPN configured.

96
docs/self-hosting.md Normal file
View File

@@ -0,0 +1,96 @@
### Self-hosting
Wiretrustee is an open-source platform that can be self-hosted on your servers.
It relies on components developed by Wiretrustee Authors [Management Service](https://github.com/wiretrustee/wiretrustee/tree/main/management), [Management UI Dashboard](https://github.com/wiretrustee/wiretrustee-dashboard), [Signal Service](https://github.com/wiretrustee/wiretrustee/tree/main/signal),
a 3rd party open-source STUN/TURN service [Coturn](https://github.com/coturn/coturn) and a 3rd party service [Auth0](https://auth0.com/).
All the components can be self-hosted except for the Auth0 service.
We chose Auth0 to "outsource" the user management part of the platform because we believe that implementing a proper user auth requires significant amount of time to make it right.
We focused on connectivity instead.
If you would like to learn more about the architecture please refer to the [Wiretrustee Architecture section](architecture.md).
### Step-by-step video guide on YouTube:
[![IMAGE ALT TEXT](https://img.youtube.com/vi/Ofpgx5WhT0k/0.jpg)](https://youtu.be/Ofpgx5WhT0k "Wiretrustee Self-Hosting Guide")
### Requirements
- Virtual machine offered by any cloud provider (e.g., AWS, DigitalOcean, Hetzner, Google Cloud, Azure ...).
- Any Linux OS.
- Docker Compose installed (see [Install Docker Compose](https://docs.docker.com/compose/install/)).
- Domain name pointing to the public IP address of your server.
- Open ports ```443, 33071, 33073, 10000, 3478``` (Dashboard, Management HTTP API, Management gRpc API, Signal gRpc, Coturn STUN/TURN respectively) on your server.
- Maybe a cup of coffee or tea :)
### Step-by-step guide
For this tutorial we will be using domain ```test.wiretrustee.com``` which points to our Ubuntu 20.04 machine hosted at Hetzner.
1. Create Auth0 account at [auth0.com](https://auth0.com/).
2. Login to your server, clone Wiretrustee repository:
```bash
git clone https://github.com/wiretrustee/wiretrustee.git wiretrustee/
```
and switch to the ```wiretrustee/infrastructure_files/``` folder that contains docker compose file:
```bash
cd wiretrustee/infrastructure_files/
```
3. Prepare configuration files.
To simplify the setup we have prepared a script to substitute required properties in the [docker-compose.yml.tmpl](../infrastructure_files/docker-compose.yml.tmpl) and [management.json.tmpl](../infrastructure_files/management.json.tmpl) files.
The [setup.env](../infrastructure_files/setup.env) file contains the following properties that have to be filled:
```bash
# e.g. app.mydomain.com
WIRETRUSTEE_DOMAIN=""
# e.g. dev-24vkclam.us.auth0.com
WIRETRUSTEE_AUTH0_DOMAIN=""
# e.g. 61u3JMXRO0oOevc7gCkZLCwePQvT4lL0
WIRETRUSTEE_AUTH0_CLIENT_ID=""
# e.g. https://app.mydomain.com/
WIRETRUSTEE_AUTH0_AUDIENCE=""
# e.g. hello@mydomain.com
WIRETRUSTEE_LETSENCRYPT_EMAIL=""
```
Please follow the steps to get the values.
4. Configure ```WIRETRUSTEE_AUTH0_DOMAIN``` ```WIRETRUSTEE_AUTH0_CLIENT_ID``` ```WIRETRUSTEE_AUTH0_AUDIENCE``` properties.
* To obtain these, please use [Auth0 React SDK Guide](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0) up until "Install the Auth0 React SDK".
:grey_exclamation: Use ```https://YOUR DOMAIN``` as ````Allowed Callback URLs````, ```Allowed Logout URLs```, ```Allowed Web Origins``` and ```Allowed Origins (CORS)```
* set the variables in the ```setup.env```
5. Configure ```WIRETRUSTEE_AUTH0_AUDIENCE``` property.
* Check [Auth0 Golang API Guide](https://auth0.com/docs/quickstart/backend/golang) to obtain AuthAudience.
* set the property in the ```setup.env``` file.
6. Configure ```WIRETRUSTEE_LETSENCRYPT_EMAIL``` property.
This can be any email address. [Let's Encrypt](https://letsencrypt.org/) will create an account while generating a new certificate.
7. Make sure all the properties set in the ```setup.env``` file and run:
```bash
./configure.sh
```
This will export all the properties as environment variables and generate ```docker-compose.yml``` and ```management.json``` files substituting required variables.
8. Run docker compose:
```bash
docker-compose up -d
```
9. Optionally check the logs by running:
```bash
docker-compose logs signal
docker-compose logs management
docker-compose logs coturn
docker-compose logs dashboard

67
go.mod
View File

@@ -1,27 +1,62 @@
module github.com/wiretrustee/wiretrustee
go 1.16
go 1.17
require (
github.com/cenkalti/backoff/v4 v4.1.0
github.com/cenkalti/backoff/v4 v4.1.2
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2
github.com/google/uuid v1.2.0
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/kardianos/service v1.2.0
github.com/onsi/ginkgo v1.16.4
github.com/onsi/gomega v1.13.0
github.com/pion/ice/v2 v2.1.7
github.com/kardianos/service v1.2.1-0.20210728001519-a323c3813bc7 //keep this version otherwise wiretrustee up command breaks
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.17.0
github.com/pion/ice/v2 v2.1.17
github.com/rs/cors v1.8.0
github.com/sirupsen/logrus v1.7.0
github.com/spf13/cobra v1.1.3
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.3.0
github.com/spf13/pflag v1.0.5
github.com/vishvananda/netlink v1.1.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
golang.zx2c4.com/wireguard/windows v0.4.5
google.golang.org/grpc v1.32.0
google.golang.org/protobuf v1.26.0
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
golang.zx2c4.com/wireguard v0.0.0-20211209221555-9c9e7e272434
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20211215182854-7a385b3431de
golang.zx2c4.com/wireguard/windows v0.5.1
google.golang.org/grpc v1.43.0
google.golang.org/protobuf v1.27.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
require github.com/rs/xid v1.3.0
require (
github.com/BurntSushi/toml v0.4.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
github.com/mdlayher/genetlink v1.1.0 // indirect
github.com/mdlayher/netlink v1.4.2 // indirect
github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/pion/dtls/v2 v2.0.12 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/stun v0.3.5 // indirect
github.com/pion/transport v0.12.3 // indirect
github.com/pion/turn/v2 v2.0.5 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
golang.org/x/mod v0.5.1 // indirect
golang.org/x/net v0.0.0-20211208012354-db4efeb81f4b // indirect
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect
golang.org/x/tools v0.1.8 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d // indirect
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
honnef.co/go/tools v0.2.2 // indirect
)

681
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -146,7 +146,7 @@ func GetListenPort(iface string) (*int, error) {
// UpdatePeer updates existing Wireguard Peer or creates a new one if doesn't exist
// Endpoint is optional
func UpdatePeer(iface string, peerKey string, allowedIps string, keepAlive time.Duration, endpoint string) error {
func UpdatePeer(iface string, peerKey string, allowedIps string, keepAlive time.Duration, endpoint string, preSharedKey *wgtypes.Key) error {
log.Debugf("updating interface %s peer %s: endpoint %s ", iface, peerKey, endpoint)
@@ -165,6 +165,7 @@ func UpdatePeer(iface string, peerKey string, allowedIps string, keepAlive time.
ReplaceAllowedIPs: true,
AllowedIPs: []net.IPNet{*ipNet},
PersistentKeepaliveInterval: &keepAlive,
PresharedKey: preSharedKey,
}
config := wgtypes.Config{

View File

@@ -111,7 +111,7 @@ func Test_UpdatePeer(t *testing.T) {
keepAlive := 15 * time.Second
allowedIP := "10.99.99.2/32"
endpoint := "127.0.0.1:9900"
err = UpdatePeer(ifaceName, peerPubKey, allowedIP, keepAlive, endpoint)
err = UpdatePeer(ifaceName, peerPubKey, allowedIP, keepAlive, endpoint, nil)
if err != nil {
t.Fatal(err)
}
@@ -163,7 +163,7 @@ func Test_UpdatePeerEndpoint(t *testing.T) {
keepAlive := 15 * time.Second
allowedIP := "10.99.99.2/32"
endpoint := "127.0.0.1:9900"
err = UpdatePeer(ifaceName, peerPubKey, allowedIP, keepAlive, endpoint)
err = UpdatePeer(ifaceName, peerPubKey, allowedIP, keepAlive, endpoint, nil)
if err != nil {
t.Fatal(err)
}
@@ -204,7 +204,7 @@ func Test_RemovePeer(t *testing.T) {
keepAlive := 15 * time.Second
allowedIP := "10.99.99.2/32"
endpoint := "127.0.0.1:9900"
err = UpdatePeer(ifaceName, peerPubKey, allowedIP, keepAlive, endpoint)
err = UpdatePeer(ifaceName, peerPubKey, allowedIP, keepAlive, endpoint, nil)
if err != nil {
t.Fatal(err)
}

View File

@@ -0,0 +1,7 @@
#!/bin/bash
unset $(grep -v '^#' ./setup.env | sed -E 's/(.*)=.*/\1/' | xargs)
export $(grep -v '^#' ./setup.env | xargs)
envsubst < docker-compose.yml.tmpl > docker-compose.yml
envsubst < management.json.tmpl > management.json

View File

@@ -1,56 +0,0 @@
version: "3"
services:
#UI dashboard
dashboard:
image: wiretrustee/dashboard:main
restart: unless-stopped
ports:
- 80:80
# - 443:443
environment:
- AUTH0_DOMAIN=<YOUR AUTH0 DOMAIN>
- AUTH0_CLIENT_ID=<YOUR AUTH0 CLIENT ID>
- AUTH0_AUDIENCE=<YOUR AUTH0 AUDIENCE>
- WIRETRUSTEE_MGMT_API_ENDPOINT=http://localhost:33071
# - NGINX_SSL_PORT: 443
# - LETSENCRYPT_DOMAIN: <YOUR DOMAIN>
# - LETSENCRYPT_EMAIL: <YOUR EMAIL>
# Signal
signal:
image: wiretrustee/signal:latest
restart: unless-stopped
volumes:
- wiretrustee-mgmt:/var/lib/wiretrustee
- /varl/log/wiretrustee/signal.log:/var/log/wiretrustee/signal.log
ports:
- 10000:10000
# # port and command for Let's Encrypt validation
# - 443:443
# command: ["--letsencrypt-domain", "<YOUR-DOMAIN>", "--log-file", "console"]
# Management
management:
image: wiretrustee/management:latest
restart: unless-stopped
volumes:
- wiretrustee-mgmt:/var/lib/wiretrustee
- ./management.json:/etc/wiretrustee/management.json
# - /var/log/wiretrustee/management.log:/var/log/wiretrustee/management.log
ports:
- 33073:33073 #gRPC port
- 33071:33071 #HTTP port
# # port and command for Let's Encrypt validation
# - 443:443
# command: ["--letsencrypt-domain", "<YOUR-DOMAIN>", "--log-file", "console"]
# Coturn
coturn:
image: coturn/coturn
restart: unless-stopped
domainname: stun.wiretrustee.com
volumes:
- ./turnserver.conf:/etc/turnserver.conf:ro
# - ./privkey.pem:/etc/coturn/private/privkey.pem:ro
# - ./cert.pem:/etc/coturn/certs/cert.pem:ro
network_mode: host
volumes:
wiretrustee-mgmt:
wiretrustee-signal:

View File

@@ -0,0 +1,61 @@
version: "3"
services:
#UI dashboard
dashboard:
image: wiretrustee/dashboard:main
restart: unless-stopped
ports:
- 80:80
- 443:443
environment:
- AUTH0_DOMAIN=$WIRETRUSTEE_AUTH0_DOMAIN
- AUTH0_CLIENT_ID=$WIRETRUSTEE_AUTH0_CLIENT_ID
- AUTH0_AUDIENCE=$WIRETRUSTEE_AUTH0_AUDIENCE
- WIRETRUSTEE_MGMT_API_ENDPOINT=https://$WIRETRUSTEE_DOMAIN:33071
- NGINX_SSL_PORT=443
- LETSENCRYPT_DOMAIN=$WIRETRUSTEE_DOMAIN
- LETSENCRYPT_EMAIL=$WIRETRUSTEE_LETSENCRYPT_EMAIL
volumes:
- /var/lib/wiretrustee/dashboard/letsencrypt:/etc/letsencrypt/
# Signal
signal:
image: wiretrustee/signal:latest
restart: unless-stopped
volumes:
- wiretrustee-signal:/var/lib/wiretrustee
# - /var/log/wiretrustee/signal.log:/var/log/wiretrustee/signal.log
ports:
- 10000:10000
# # port and command for Let's Encrypt validation
# - 443:443
# command: ["--letsencrypt-domain", "$WIRETRUSTEE_DOMAIN", "--log-file", "console"]
# Management
management:
image: wiretrustee/management:latest
restart: unless-stopped
depends_on:
- dashboard
volumes:
- wiretrustee-mgmt:/var/lib/wiretrustee
- /var/lib/wiretrustee/dashboard/letsencrypt:/etc/letsencrypt:ro
- ./management.json:/etc/wiretrustee/management.json
# - /var/log/wiretrustee/management.log:/var/log/wiretrustee/management.log
ports:
- 33073:33073 #gRPC port
- 33071:33071 #HTTP port
# # port and command for Let's Encrypt validation
# - 443:443
# command: ["--letsencrypt-domain", "$WIRETRUSTEE_DOMAIN", "--log-file", "console"]
# Coturn
coturn:
image: coturn/coturn
restart: unless-stopped
domainname: <YOUR DOMAIN>
volumes:
- ./turnserver.conf:/etc/turnserver.conf:ro
# - ./privkey.pem:/etc/coturn/private/privkey.pem:ro
# - ./cert.pem:/etc/coturn/certs/cert.pem:ro
network_mode: host
volumes:
wiretrustee-mgmt:
wiretrustee-signal:

View File

@@ -1,37 +0,0 @@
{
"Stuns": [
{
"Proto": "udp",
"URI": "stun:stun.wiretrustee.com:3468",
"Username": "",
"Password": null
}
],
"TURNConfig": {
"Turns": [
{
"Proto": "udp",
"URI": "turn:stun.wiretrustee.com:3468",
"Username": "some_user",
"Password": "c29tZV9wYXNzd29yZA=="
}
],
"CredentialsTTL": "1h",
"Secret": "c29tZV9wYXNzd29yZA==",
"TimeBasedCredentials": true
},
"Signal": {
"Proto": "http",
"URI": "signal.wiretrustee.com:10000",
"Username": "",
"Password": null
},
"Datadir": "",
"HttpConfig": {
"LetsEncryptDomain": "<PASTE YOUR LET'S ENCRYPT DOMAIN HERE>",
"Address": "0.0.0.0:33071",
"AuthIssuer": "<PASTE YOUR AUTH0 ISSUER HERE>,",
"AuthAudience": "<PASTE YOUR AUTH0 AUDIENCE HERE>",
"AuthKeysLocation": "<PASTE YOUR AUTH0 PUBLIC JWT KEYS LOCATION HERE>"
}
}

View File

@@ -0,0 +1,39 @@
{
"Stuns": [
{
"Proto": "udp",
"URI": "stun:$WIRETRUSTEE_DOMAIN:3478",
"Username": "",
"Password": null
}
],
"TURNConfig": {
"Turns": [
{
"Proto": "udp",
"URI": "turn:$WIRETRUSTEE_DOMAIN:3478",
"Username": "",
"Password": null
}
],
"CredentialsTTL": "12h",
"Secret": "secret",
"TimeBasedCredentials": false
},
"Signal": {
"Proto": "http",
"URI": "$WIRETRUSTEE_DOMAIN:10000",
"Username": "",
"Password": null
},
"Datadir": "",
"HttpConfig": {
"LetsEncryptDomain": "",
"CertFile":"/etc/letsencrypt/live/$WIRETRUSTEE_DOMAIN/fullchain.pem",
"CertKey":"/etc/letsencrypt/live/$WIRETRUSTEE_DOMAIN/privkey.pem",
"Address": "0.0.0.0:33071",
"AuthIssuer": "https://$WIRETRUSTEE_AUTH0_DOMAIN/",
"AuthAudience": "$WIRETRUSTEE_AUTH0_AUDIENCE",
"AuthKeysLocation": "https://$WIRETRUSTEE_AUTH0_DOMAIN/.well-known/jwks.json"
}
}

View File

@@ -0,0 +1,10 @@
# e.g. app.mydomain.com
WIRETRUSTEE_DOMAIN=""
# e.g. dev-24vkclam.us.auth0.com
WIRETRUSTEE_AUTH0_DOMAIN=""
# e.g. 61u3JMXRO0oOevc7gCkZLCwePQvT4lL0
WIRETRUSTEE_AUTH0_CLIENT_ID=""
# e.g. https://app.mydomain.com/
WIRETRUSTEE_AUTH0_AUDIENCE=""
# e.g. hello@mydomain.com
WIRETRUSTEE_LETSENCRYPT_EMAIL=""

View File

@@ -14,7 +14,8 @@ Flags:
-h, --help help for management
--letsencrypt-domain string a domain to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS
--port int server port to listen on (default 33073)
--cert-file string Location of your SSL certificate. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect
--cert-key string Location of your SSL certificate private key. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect
Global Flags:
--config string Wiretrustee config file location to write new config to (default "/etc/wiretrustee/config.json")
--log-level string (default "info")

View File

@@ -3,6 +3,7 @@ package client
import (
"context"
"crypto/tls"
"fmt"
"github.com/cenkalti/backoff/v4"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/client/system"
@@ -10,7 +11,9 @@ import (
"github.com/wiretrustee/wiretrustee/management/proto"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
"io"
"time"
@@ -26,13 +29,13 @@ type Client struct {
// NewClient creates a new client to Management service
func NewClient(ctx context.Context, addr string, ourPrivateKey wgtypes.Key, tlsEnabled bool) (*Client, error) {
transportOption := grpc.WithInsecure()
transportOption := grpc.WithTransportCredentials(insecure.NewCredentials())
if tlsEnabled {
transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))
}
mgmCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
mgmCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
conn, err := grpc.DialContext(
mgmCtx,
@@ -40,12 +43,12 @@ func NewClient(ctx context.Context, addr string, ourPrivateKey wgtypes.Key, tlsE
transportOption,
grpc.WithBlock(),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 3 * time.Second,
Timeout: 2 * time.Second,
Time: 15 * time.Second,
Timeout: 10 * time.Second,
}))
if err != nil {
log.Errorf("failed creating connection to Management Srvice %v", err)
log.Errorf("failed creating connection to Management Service %v", err)
return nil, err
}
@@ -65,26 +68,38 @@ func (c *Client) Close() error {
}
//defaultBackoff is a basic backoff mechanism for general issues
func defaultBackoff() backoff.BackOff {
return &backoff.ExponentialBackOff{
func defaultBackoff(ctx context.Context) backoff.BackOff {
return backoff.WithContext(&backoff.ExponentialBackOff{
InitialInterval: 800 * time.Millisecond,
RandomizationFactor: backoff.DefaultRandomizationFactor,
Multiplier: backoff.DefaultMultiplier,
MaxInterval: 30 * time.Second,
MaxElapsedTime: 24 * 3 * time.Hour, //stop after 3 days trying
MaxInterval: 10 * time.Second,
MaxElapsedTime: 12 * time.Hour, //stop after 12 hours of trying, the error will be propagated to the general retry of the client
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}
}, ctx)
}
// ready indicates whether the client is okay and ready to be used
// for now it just checks whether gRPC connection to the service is ready
func (c *Client) ready() bool {
return c.conn.GetState() == connectivity.Ready
}
// Sync wraps the real client's Sync endpoint call and takes care of retries and encryption/decryption of messages
// Blocking request. The result will be sent via msgHandler callback function
func (c *Client) Sync(msgHandler func(msg *proto.SyncResponse) error) error {
var backOff = defaultBackoff()
var backOff = defaultBackoff(c.ctx)
operation := func() error {
log.Debugf("management connection state %v", c.conn.GetState())
if !c.ready() {
return fmt.Errorf("no connection to management")
}
// todo we already have it since we did the Login, maybe cache it locally?
serverPubKey, err := c.GetServerPublicKey()
if err != nil {
@@ -98,23 +113,21 @@ func (c *Client) Sync(msgHandler func(msg *proto.SyncResponse) error) error {
return err
}
log.Infof("connected to the Management Service Stream")
log.Infof("connected to the Management Service stream")
// blocking until error
err = c.receiveEvents(stream, *serverPubKey, msgHandler)
if err != nil {
/*if errStatus, ok := status.FromError(err); ok && errStatus.Code() == codes.PermissionDenied {
//todo handle differently??
}*/
backOff.Reset()
return err
}
backOff.Reset()
return nil
}
err := backoff.Retry(operation, backOff)
if err != nil {
log.Errorf("exiting Management Service connection retry loop due to unrecoverable error %s ", err)
log.Warnf("exiting Management Service connection retry loop due to unrecoverable error: %s", err)
return err
}
@@ -141,11 +154,11 @@ func (c *Client) receiveEvents(stream proto.ManagementService_SyncClient, server
for {
update, err := stream.Recv()
if err == io.EOF {
log.Errorf("managment stream was closed: %s", err)
log.Errorf("Management stream has been closed by server: %s", err)
return err
}
if err != nil {
log.Errorf("disconnected from Management Service sync stream: %v", err)
log.Warnf("disconnected from Management Service sync stream: %v", err)
return err
}
@@ -159,7 +172,7 @@ func (c *Client) receiveEvents(stream proto.ManagementService_SyncClient, server
err = msgHandler(decryptedResp)
if err != nil {
log.Errorf("failed handling an update message received from Management Service %v", err.Error())
log.Errorf("failed handling an update message received from Management Service: %v", err.Error())
return err
}
}
@@ -167,6 +180,10 @@ func (c *Client) receiveEvents(stream proto.ManagementService_SyncClient, server
// GetServerPublicKey returns server Wireguard public key (used later for encrypting messages sent to the server)
func (c *Client) GetServerPublicKey() (*wgtypes.Key, error) {
if !c.ready() {
return nil, fmt.Errorf("no connection to management")
}
mgmCtx, cancel := context.WithTimeout(c.ctx, 5*time.Second) //todo make a general setting
defer cancel()
resp, err := c.realClient.GetServerKey(mgmCtx, &proto.Empty{})
@@ -183,6 +200,9 @@ func (c *Client) GetServerPublicKey() (*wgtypes.Key, error) {
}
func (c *Client) login(serverKey wgtypes.Key, req *proto.LoginRequest) (*proto.LoginResponse, error) {
if !c.ready() {
return nil, fmt.Errorf("no connection to management")
}
loginReq, err := encryption.EncryptMessage(serverKey, c.key, req)
if err != nil {
log.Errorf("failed to encrypt message: %s", err)

View File

@@ -2,6 +2,7 @@ package cmd
import (
"context"
"crypto/tls"
"flag"
"fmt"
"github.com/wiretrustee/wiretrustee/management/server"
@@ -25,6 +26,8 @@ var (
mgmtDataDir string
mgmtConfig string
mgmtLetsencryptDomain string
certFile string
certKey string
kaep = keepalive.EnforcementPolicy{
MinTime: 15 * time.Second,
@@ -71,12 +74,23 @@ var (
var httpServer *http.Server
if config.HttpConfig.LetsEncryptDomain != "" {
//automatically generate a new certificate with Let's Encrypt
certManager := encryption.CreateCertManager(config.Datadir, config.HttpConfig.LetsEncryptDomain)
transportCredentials := credentials.NewTLS(certManager.TLSConfig())
opts = append(opts, grpc.Creds(transportCredentials))
httpServer = http.NewHttpsServer(config.HttpConfig, certManager, accountManager)
} else if config.HttpConfig.CertFile != "" && config.HttpConfig.CertKey != "" {
//use provided certificate
tlsConfig, err := loadTLSConfig(config.HttpConfig.CertFile, config.HttpConfig.CertKey)
if err != nil {
log.Fatal("cannot load TLS credentials: ", err)
}
transportCredentials := credentials.NewTLS(tlsConfig)
opts = append(opts, grpc.Creds(transportCredentials))
httpServer = http.NewHttpsServerWithTLSConfig(config.HttpConfig, tlsConfig, accountManager)
} else {
//start server without SSL
httpServer = http.NewHttpServer(config.HttpConfig, accountManager)
}
@@ -136,14 +150,37 @@ func loadConfig() (*server.Config, error) {
config.Datadir = mgmtDataDir
}
if certKey != "" && certFile != "" {
config.HttpConfig.CertFile = certFile
config.HttpConfig.CertKey = certKey
}
return config, err
}
func loadTLSConfig(certFile string, certKey string) (*tls.Config, error) {
// Load server's certificate and private key
serverCert, err := tls.LoadX509KeyPair(certFile, certKey)
if err != nil {
return nil, err
}
// Create the credentials and return it
config := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.NoClientCert,
}
return config, nil
}
func init() {
mgmtCmd.Flags().IntVar(&mgmtPort, "port", 33073, "server port to listen on")
mgmtCmd.Flags().StringVar(&mgmtDataDir, "datadir", "/var/lib/wiretrustee/", "server data directory location")
mgmtCmd.Flags().StringVar(&mgmtConfig, "config", "/etc/wiretrustee/management.json", "Wiretrustee config file location. Config params specified via command line (e.g. datadir) have a precedence over configuration from this file")
mgmtCmd.Flags().StringVar(&mgmtLetsencryptDomain, "letsencrypt-domain", "", "a domain to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS")
mgmtCmd.Flags().StringVar(&certFile, "cert-file", "", "Location of your SSL certificate. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
mgmtCmd.Flags().StringVar(&certKey, "cert-key", "", "Location of your SSL certificate private key. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
rootCmd.MarkFlagRequired("config") //nolint

View File

@@ -2,12 +2,13 @@ package server
import (
"github.com/google/uuid"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/util"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net"
"sync"
"time"
)
type AccountManager struct {
@@ -19,10 +20,39 @@ type AccountManager struct {
// Account represents a unique account of the system
type Account struct {
Id string
Id string
// User.Id it was created by
CreatedBy string
SetupKeys map[string]*SetupKey
Network *Network
Peers map[string]*Peer
Users map[string]*User
}
func (a *Account) Copy() *Account {
peers := map[string]*Peer{}
for id, peer := range a.Peers {
peers[id] = peer.Copy()
}
users := map[string]*User{}
for id, user := range a.Users {
users[id] = user.Copy()
}
setupKeys := map[string]*SetupKey{}
for id, key := range a.SetupKeys {
setupKeys[id] = key.Copy()
}
return &Account{
Id: a.Id,
CreatedBy: a.CreatedBy,
SetupKeys: setupKeys,
Network: a.Network.Copy(),
Peers: peers,
Users: users,
}
}
// NewManager creates a new AccountManager with a provided Store
@@ -35,16 +65,21 @@ func NewManager(store Store, peersUpdateManager *PeersUpdateManager) *AccountMan
}
//AddSetupKey generates a new setup key with a given name and type, and adds it to the specified account
func (am *AccountManager) AddSetupKey(accountId string, keyName string, keyType SetupKeyType, expiresIn time.Duration) (*SetupKey, error) {
func (am *AccountManager) AddSetupKey(accountId string, keyName string, keyType SetupKeyType, expiresIn *util.Duration) (*SetupKey, error) {
am.mux.Lock()
defer am.mux.Unlock()
keyDuration := DefaultSetupKeyDuration
if expiresIn != nil {
keyDuration = expiresIn.Duration
}
account, err := am.Store.GetAccount(accountId)
if err != nil {
return nil, status.Errorf(codes.NotFound, "account not found")
}
setupKey := GenerateSetupKey(keyName, keyType, expiresIn)
setupKey := GenerateSetupKey(keyName, keyType, keyDuration)
account.SetupKeys[setupKey.Key] = setupKey
err = am.Store.SaveAccount(account)
@@ -120,29 +155,6 @@ func (am *AccountManager) GetAccount(accountId string) (*Account, error) {
return account, nil
}
// GetOrCreateAccount returns an existing account or creates a new one if doesn't exist
func (am *AccountManager) GetOrCreateAccount(accountId string) (*Account, error) {
am.mux.Lock()
defer am.mux.Unlock()
_, err := am.Store.GetAccount(accountId)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
return am.createAccount(accountId)
} else {
// other error
return nil, err
}
}
account, err := am.Store.GetAccount(accountId)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed retrieving account")
}
return account, nil
}
//AccountExists checks whether account exists (returns true) or not (returns false)
func (am *AccountManager) AccountExists(accountId string) (*bool, error) {
am.mux.Lock()
@@ -163,18 +175,18 @@ func (am *AccountManager) AccountExists(accountId string) (*bool, error) {
return &res, nil
}
// AddAccount generates a new Account with a provided accountId and saves to the Store
func (am *AccountManager) AddAccount(accountId string) (*Account, error) {
// AddAccount generates a new Account with a provided accountId and userId, saves to the Store
func (am *AccountManager) AddAccount(accountId string, userId string) (*Account, error) {
am.mux.Lock()
defer am.mux.Unlock()
return am.createAccount(accountId)
return am.createAccount(accountId, userId)
}
func (am *AccountManager) createAccount(accountId string) (*Account, error) {
account, _ := newAccountWithId(accountId)
func (am *AccountManager) createAccount(accountId string, userId string) (*Account, error) {
account, _ := newAccountWithId(accountId, userId)
err := am.Store.SaveAccount(account)
if err != nil {
@@ -185,7 +197,7 @@ func (am *AccountManager) createAccount(accountId string) (*Account, error) {
}
// newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id
func newAccountWithId(accountId string) (*Account, *SetupKey) {
func newAccountWithId(accountId string, userId string) (*Account, *SetupKey) {
log.Debugf("creating new account")
@@ -199,16 +211,17 @@ func newAccountWithId(accountId string) (*Account, *SetupKey) {
Net: net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}},
Dns: ""}
peers := make(map[string]*Peer)
users := make(map[string]*User)
log.Debugf("created new account %s with setup key %s", accountId, defaultKey.Key)
return &Account{Id: accountId, SetupKeys: setupKeys, Network: network, Peers: peers}, defaultKey
return &Account{Id: accountId, SetupKeys: setupKeys, Network: network, Peers: peers, Users: users, CreatedBy: userId}, defaultKey
}
// newAccount creates a new Account with a default SetupKey (doesn't store in a Store)
func newAccount() (*Account, *SetupKey) {
accountId := uuid.New().String()
return newAccountWithId(accountId)
// newAccount creates a new Account with a default SetupKey and a provided User.Id of a user who issued account creation (doesn't store in a Store)
func newAccount(userId string) (*Account, *SetupKey) {
accountId := xid.New().String()
return newAccountWithId(accountId, userId)
}
func getAccountSetupKeyById(acc *Account, keyId string) *SetupKey {

View File

@@ -2,12 +2,36 @@ package server
import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net"
"testing"
)
func TestAccountManager_GetOrCreateAccountByUser(t *testing.T) {
manager, err := createManager(t)
if err != nil {
t.Fatal(err)
return
}
userId := "test_user"
account, err := manager.GetOrCreateAccountByUser(userId)
if err != nil {
t.Fatal(err)
}
if account == nil {
t.Fatalf("expected to create an account for a user %s", userId)
}
account, err = manager.GetAccountByUser(userId)
if err != nil {
t.Errorf("expected to get existing account after creation, no account was found for a user %s", userId)
}
if account != nil && account.Users[userId] == nil {
t.Fatalf("expected to create an account for a user %s but no user was found after creation udner the account %s", userId, account.Id)
}
}
func TestAccountManager_AddAccount(t *testing.T) {
manager, err := createManager(t)
if err != nil {
@@ -16,6 +40,7 @@ func TestAccountManager_AddAccount(t *testing.T) {
}
expectedId := "test_account"
userId := "account_creator"
expectedPeersSize := 0
expectedSetupKeysSize := 2
expectedNetwork := net.IPNet{
@@ -23,7 +48,7 @@ func TestAccountManager_AddAccount(t *testing.T) {
Mask: net.IPMask{255, 192, 0, 0},
}
account, err := manager.AddAccount(expectedId)
account, err := manager.AddAccount(expectedId, userId)
if err != nil {
t.Fatal(err)
}
@@ -45,46 +70,6 @@ func TestAccountManager_AddAccount(t *testing.T) {
}
}
func TestAccountManager_GetOrCreateAccount(t *testing.T) {
manager, err := createManager(t)
if err != nil {
t.Fatal(err)
return
}
expectedId := "test_account"
//make sure account doesn't exist
account, err := manager.GetAccount(expectedId)
if err != nil {
errStatus, ok := status.FromError(err)
if !(ok && errStatus.Code() == codes.NotFound) {
t.Fatal(err)
}
}
if account != nil {
t.Fatal("expecting empty account")
}
account, err = manager.GetOrCreateAccount(expectedId)
if err != nil {
t.Fatal(err)
}
if account.Id != expectedId {
t.Fatalf("expected to create an account, got wrong account")
}
account, err = manager.GetOrCreateAccount(expectedId)
if err != nil {
t.Errorf("expected to get existing account after creation, failed")
}
if account.Id != expectedId {
t.Fatalf("expected to create an account, got wrong account")
}
}
func TestAccountManager_AccountExists(t *testing.T) {
manager, err := createManager(t)
if err != nil {
@@ -93,7 +78,8 @@ func TestAccountManager_AccountExists(t *testing.T) {
}
expectedId := "test_account"
_, err = manager.AddAccount(expectedId)
userId := "account_creator"
_, err = manager.AddAccount(expectedId, userId)
if err != nil {
t.Fatal(err)
}
@@ -117,7 +103,8 @@ func TestAccountManager_GetAccount(t *testing.T) {
}
expectedId := "test_account"
account, err := manager.AddAccount(expectedId)
userId := "account_creator"
account, err := manager.AddAccount(expectedId, userId)
if err != nil {
t.Fatal(err)
}
@@ -154,7 +141,7 @@ func TestAccountManager_AddPeer(t *testing.T) {
return
}
account, err := manager.AddAccount("test_account")
account, err := manager.AddAccount("test_account", "account_creator")
if err != nil {
t.Fatal(err)
}

View File

@@ -36,7 +36,11 @@ type TURNConfig struct {
// HttpServerConfig is a config of the HTTP Management service server
type HttpServerConfig struct {
LetsEncryptDomain string
Address string
//CertFile is the location of the certificate
CertFile string
//CertKey is the location of the certificate private key
CertKey string
Address string
// AuthAudience identifies the recipients that the JWT is intended for (aud in JWT)
AuthAudience string
// AuthIssuer identifies principal that issued the JWT.

View File

@@ -20,6 +20,7 @@ type FileStore struct {
Accounts map[string]*Account
SetupKeyId2AccountId map[string]string `json:"-"`
PeerKeyId2AccountId map[string]string `json:"-"`
UserId2AccountId map[string]string `json:"-"`
// mutex to synchronise Store read/write operations
mux sync.Mutex `json:"-"`
@@ -45,6 +46,7 @@ func restore(file string) (*FileStore, error) {
mux: sync.Mutex{},
SetupKeyId2AccountId: make(map[string]string),
PeerKeyId2AccountId: make(map[string]string),
UserId2AccountId: make(map[string]string),
storeFile: file,
}
@@ -65,6 +67,7 @@ func restore(file string) (*FileStore, error) {
store.storeFile = file
store.SetupKeyId2AccountId = make(map[string]string)
store.PeerKeyId2AccountId = make(map[string]string)
store.UserId2AccountId = make(map[string]string)
for accountId, account := range store.Accounts {
for setupKeyId := range account.SetupKeys {
store.SetupKeyId2AccountId[strings.ToUpper(setupKeyId)] = accountId
@@ -72,6 +75,9 @@ func restore(file string) (*FileStore, error) {
for _, peer := range account.Peers {
store.PeerKeyId2AccountId[peer.Key] = accountId
}
for _, user := range account.Users {
store.UserId2AccountId[user.Id] = accountId
}
}
return store, nil
@@ -168,6 +174,10 @@ func (s *FileStore) SaveAccount(account *Account) error {
s.PeerKeyId2AccountId[peer.Key] = account.Id
}
for _, user := range account.Users {
s.UserId2AccountId[user.Id] = account.Id
}
err := s.persist(s.storeFile)
if err != nil {
return err
@@ -217,6 +227,18 @@ func (s *FileStore) GetAccount(accountId string) (*Account, error) {
return account, nil
}
func (s *FileStore) GetUserAccount(userId string) (*Account, error) {
s.mux.Lock()
defer s.mux.Unlock()
accountId, accountIdFound := s.UserId2AccountId[userId]
if !accountIdFound {
return nil, status.Errorf(codes.NotFound, "account not found")
}
return s.GetAccount(accountId)
}
func (s *FileStore) GetPeerAccount(peerKey string) (*Account, error) {
s.mux.Lock()
defer s.mux.Unlock()

View File

@@ -0,0 +1,171 @@
package server
import (
"github.com/wiretrustee/wiretrustee/util"
"net"
"path/filepath"
"testing"
"time"
)
func TestNewStore(t *testing.T) {
store := newStore(t)
if store.Accounts == nil || len(store.Accounts) != 0 {
t.Errorf("expected to create a new empty Accounts map when creating a new FileStore")
}
if store.SetupKeyId2AccountId == nil || len(store.SetupKeyId2AccountId) != 0 {
t.Errorf("expected to create a new empty SetupKeyId2AccountId map when creating a new FileStore")
}
if store.PeerKeyId2AccountId == nil || len(store.PeerKeyId2AccountId) != 0 {
t.Errorf("expected to create a new empty PeerKeyId2AccountId map when creating a new FileStore")
}
if store.UserId2AccountId == nil || len(store.UserId2AccountId) != 0 {
t.Errorf("expected to create a new empty UserId2AccountId map when creating a new FileStore")
}
}
func TestSaveAccount(t *testing.T) {
store := newStore(t)
account, _ := newAccount("testuser")
account.Users["testuser"] = NewAdminUser("testuser")
setupKey := GenerateDefaultSetupKey()
account.SetupKeys[setupKey.Key] = setupKey
account.Peers["testpeer"] = &Peer{
Key: "peerkey",
SetupKey: "peerkeysetupkey",
IP: net.IP{127, 0, 0, 1},
Meta: PeerSystemMeta{},
Name: "peer name",
Status: &PeerStatus{Connected: true, LastSeen: time.Now()},
}
// SaveAccount should trigger persist
err := store.SaveAccount(account)
if err != nil {
return
}
if store.Accounts[account.Id] == nil {
t.Errorf("expecting Account to be stored after SaveAccount()")
}
if store.PeerKeyId2AccountId["peerkey"] == "" {
t.Errorf("expecting PeerKeyId2AccountId index updated after SaveAccount()")
}
if store.UserId2AccountId["testuser"] == "" {
t.Errorf("expecting UserId2AccountId index updated after SaveAccount()")
}
if store.SetupKeyId2AccountId[setupKey.Key] == "" {
t.Errorf("expecting SetupKeyId2AccountId index updated after SaveAccount()")
}
}
func TestStore(t *testing.T) {
store := newStore(t)
account, _ := newAccount("testuser")
account.Users["testuser"] = NewAdminUser("testuser")
account.Peers["testpeer"] = &Peer{
Key: "peerkey",
SetupKey: "peerkeysetupkey",
IP: net.IP{127, 0, 0, 1},
Meta: PeerSystemMeta{},
Name: "peer name",
Status: &PeerStatus{Connected: true, LastSeen: time.Now()},
}
// SaveAccount should trigger persist
err := store.SaveAccount(account)
if err != nil {
return
}
restored, err := NewStore(store.storeFile)
if err != nil {
return
}
restoredAccount := restored.Accounts[account.Id]
if restoredAccount == nil {
t.Errorf("failed to restore a FileStore file - missing Account %s", account.Id)
}
if restoredAccount != nil && restoredAccount.Peers["testpeer"] == nil {
t.Errorf("failed to restore a FileStore file - missing Peer testpeer")
}
if restoredAccount != nil && restoredAccount.CreatedBy != "testuser" {
t.Errorf("failed to restore a FileStore file - missing Account CreatedBy")
}
if restoredAccount != nil && restoredAccount.Users["testuser"] == nil {
t.Errorf("failed to restore a FileStore file - missing User testuser")
}
if restoredAccount != nil && restoredAccount.Network == nil {
t.Errorf("failed to restore a FileStore file - missing Network")
}
}
func TestRestore(t *testing.T) {
storeDir := t.TempDir()
err := util.CopyFileContents("testdata/store.json", filepath.Join(storeDir, "store.json"))
if err != nil {
t.Fatal(err)
}
store, err := NewStore(storeDir)
if err != nil {
return
}
account := store.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"]
if account == nil {
t.Errorf("failed to restore a FileStore file - missing account bf1c8084-ba50-4ce7-9439-34653001fc3b")
}
if account != nil && account.Users["edafee4e-63fb-11ec-90d6-0242ac120003"] == nil {
t.Errorf("failed to restore a FileStore file - missing Account User edafee4e-63fb-11ec-90d6-0242ac120003")
}
if account != nil && account.Users["f4f6d672-63fb-11ec-90d6-0242ac120003"] == nil {
t.Errorf("failed to restore a FileStore file - missing Account User f4f6d672-63fb-11ec-90d6-0242ac120003")
}
if account != nil && account.Network == nil {
t.Errorf("failed to restore a FileStore file - missing Account Network")
}
if account != nil && account.SetupKeys["A2C8E62B-38F5-4553-B31E-DD66C696CEBB"] == nil {
t.Errorf("failed to restore a FileStore file - missing Account SetupKey A2C8E62B-38F5-4553-B31E-DD66C696CEBB")
}
if len(store.UserId2AccountId) != 2 {
t.Errorf("failed to restore a FileStore wrong UserId2AccountId mapping")
}
if len(store.SetupKeyId2AccountId) != 1 {
t.Errorf("failed to restore a FileStore wrong SetupKeyId2AccountId mapping")
}
}
func newStore(t *testing.T) *FileStore {
store, err := NewStore(t.TempDir())
if err != nil {
t.Errorf("failed creating a new store")
}
return store
}

View File

@@ -62,7 +62,13 @@ func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseW
}
func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
accountId := extractAccountIdFromRequestContext(r)
userId := extractUserIdFromRequestContext(r)
account, err := h.accountManager.GetOrCreateAccountByUser(userId)
if err != nil {
log.Errorf("failed getting account of a user %s: %v", userId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
vars := mux.Vars(r)
peerId := vars["id"] //effectively peer IP address
if len(peerId) == 0 {
@@ -70,7 +76,7 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
return
}
peer, err := h.accountManager.GetPeerByIP(accountId, peerId)
peer, err := h.accountManager.GetPeerByIP(account.Id, peerId)
if err != nil {
http.Error(w, "peer not found", http.StatusNotFound)
return
@@ -78,10 +84,10 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodDelete:
h.deletePeer(accountId, peer, w, r)
h.deletePeer(account.Id, peer, w, r)
return
case http.MethodPut:
h.updatePeer(accountId, peer, w, r)
h.updatePeer(account.Id, peer, w, r)
return
case http.MethodGet:
writeJSONObject(w, toPeerResponse(peer))
@@ -96,11 +102,11 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
accountId := extractAccountIdFromRequestContext(r)
userId := extractUserIdFromRequestContext(r)
//new user -> create a new account
account, err := h.accountManager.GetOrCreateAccount(accountId)
account, err := h.accountManager.GetOrCreateAccountByUser(userId)
if err != nil {
log.Errorf("failed getting user account %s: %v", accountId, err)
log.Errorf("failed getting account of a user %s: %v", userId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/util"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net/http"
@@ -34,7 +35,7 @@ type SetupKeyResponse struct {
type SetupKeyRequest struct {
Name string
Type server.SetupKeyType
ExpiresIn Duration
ExpiresIn *util.Duration
Revoked bool
}
@@ -102,7 +103,7 @@ func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.R
return
}
setupKey, err := h.accountManager.AddSetupKey(accountId, req.Name, req.Type, req.ExpiresIn.Duration)
setupKey, err := h.accountManager.AddSetupKey(accountId, req.Name, req.Type, req.ExpiresIn)
if err != nil {
errStatus, ok := status.FromError(err)
if ok && errStatus.Code() == codes.NotFound {
@@ -117,7 +118,14 @@ func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.R
}
func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) {
accountId := extractAccountIdFromRequestContext(r)
userId := extractUserIdFromRequestContext(r)
account, err := h.accountManager.GetOrCreateAccountByUser(userId)
if err != nil {
log.Errorf("failed getting account of a user %s: %v", userId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
vars := mux.Vars(r)
keyId := vars["id"]
if len(keyId) == 0 {
@@ -127,10 +135,10 @@ func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPut:
h.updateKey(accountId, keyId, w, r)
h.updateKey(account.Id, keyId, w, r)
return
case http.MethodGet:
h.getKey(accountId, keyId, w, r)
h.getKey(account.Id, keyId, w, r)
return
default:
http.Error(w, "", http.StatusNotFound)
@@ -139,21 +147,20 @@ func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) {
func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) {
accountId := extractAccountIdFromRequestContext(r)
userId := extractUserIdFromRequestContext(r)
//new user -> create a new account
account, err := h.accountManager.GetOrCreateAccountByUser(userId)
if err != nil {
log.Errorf("failed getting account of a user %s: %v", userId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
switch r.Method {
case http.MethodPost:
h.createKey(accountId, w, r)
h.createKey(account.Id, w, r)
return
case http.MethodGet:
//new user -> create a new account
account, err := h.accountManager.GetOrCreateAccount(accountId)
if err != nil {
log.Errorf("failed getting user account %s: %v", accountId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
@@ -164,7 +171,7 @@ func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) {
err = json.NewEncoder(w).Encode(respBody)
if err != nil {
log.Errorf("failed encoding account peers %s: %v", accountId, err)
log.Errorf("failed encoding account peers %s: %v", account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}

View File

@@ -8,8 +8,8 @@ import (
"time"
)
// extractAccountIdFromRequestContext extracts accountId from the request context previously filled by the JWT token (after auth)
func extractAccountIdFromRequestContext(r *http.Request) string {
// extractUserIdFromRequestContext extracts accountId from the request context previously filled by the JWT token (after auth)
func extractUserIdFromRequestContext(r *http.Request) string {
token := r.Context().Value("user").(*jwt.Token)
claims := token.Claims.(jwt.MapClaims)

View File

@@ -2,6 +2,7 @@ package http
import (
"context"
"crypto/tls"
"github.com/gorilla/mux"
"github.com/rs/cors"
log "github.com/sirupsen/logrus"
@@ -17,10 +18,11 @@ type Server struct {
server *http.Server
config *s.HttpServerConfig
certManager *autocert.Manager
tlsConfig *tls.Config
accountManager *s.AccountManager
}
// NewHttpsServer creates a new HTTPs server (with HTTPS support)
// NewHttpsServer creates a new HTTPs server (with HTTPS support) and a certManager that is responsible for generating and renewing Let's Encrypt certificate
// The listening address will be :443 no matter what was specified in s.HttpServerConfig.Address
func NewHttpsServer(config *s.HttpServerConfig, certManager *autocert.Manager, accountManager *s.AccountManager) *Server {
server := &http.Server{
@@ -32,6 +34,18 @@ func NewHttpsServer(config *s.HttpServerConfig, certManager *autocert.Manager, a
return &Server{server: server, config: config, certManager: certManager, accountManager: accountManager}
}
// NewHttpsServerWithTLSConfig creates a new HTTPs server with a provided tls.Config.
// Usually used when you already have a certificate
func NewHttpsServerWithTLSConfig(config *s.HttpServerConfig, tlsConfig *tls.Config, accountManager *s.AccountManager) *Server {
server := &http.Server{
Addr: config.Address,
WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60,
}
return &Server{server: server, config: config, tlsConfig: tlsConfig, accountManager: accountManager}
}
// NewHttpServer creates a new HTTP server (without HTTPS)
func NewHttpServer(config *s.HttpServerConfig, accountManager *s.AccountManager) *Server {
return NewHttpsServer(config, nil, accountManager)
@@ -71,13 +85,26 @@ func (s *Server) Start() error {
if s.certManager != nil {
// if HTTPS is enabled we reuse the listener from the cert manager
listener := s.certManager.Listener()
log.Infof("http server listening on %s", listener.Addr())
log.Infof("HTTPs server listening on %s with Let's Encrypt autocert configured", listener.Addr())
if err = http.Serve(listener, s.certManager.HTTPHandler(r)); err != nil {
log.Errorf("failed to serve https server: %v", err)
return err
}
} else if s.tlsConfig != nil {
listener, err := tls.Listen("tcp", s.config.Address, s.tlsConfig)
if err != nil {
log.Errorf("failed to serve https server: %v", err)
return err
}
log.Infof("HTTPs server listening on %s", listener.Addr())
if err = http.Serve(listener, r); err != nil {
log.Errorf("failed to serve https server: %v", err)
return err
}
} else {
log.Infof("http server listening on %s", s.server.Addr)
log.Infof("HTTP server listening on %s", s.server.Addr)
if err = s.server.ListenAndServe(); err != nil {
log.Errorf("failed to serve http server: %v", err)
return err

View File

@@ -3,6 +3,7 @@ package server_test
import (
"context"
server "github.com/wiretrustee/wiretrustee/management/server"
"google.golang.org/grpc/credentials/insecure"
"io/ioutil"
"math/rand"
"net"
@@ -472,7 +473,9 @@ func loginPeerWithValidSetupKey(serverPubKey wgtypes.Key, key wgtypes.Key, clien
func createRawClient(addr string) (mgmtProto.ManagementServiceClient, *grpc.ClientConn) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := grpc.DialContext(ctx, addr, grpc.WithInsecure(),
conn, err := grpc.DialContext(ctx, addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 10 * time.Second,

View File

@@ -0,0 +1,13 @@
## Migration from Store v2 to Store v2
Previously Account.Id was an Auth0 user id.
Conversion moves user id to Account.CreatedBy and generates a new Account.Id using xid.
It also adds a User with id = old Account.Id with a role Admin.
To start a conversion simply run the command below providing your current Wiretrustee Management datadir (where store.json file is located)
and a new data directory location (where a converted store.js will be stored):
```shell
./migration --oldDir /var/wiretrustee/datadir --newDir /var/wiretrustee/newdatadir/
```
Afterwards you can run the Management service providing ```/var/wiretrustee/newdatadir/ ``` as a datadir.

View File

@@ -0,0 +1,56 @@
package main
import (
"flag"
"fmt"
"github.com/rs/xid"
"github.com/wiretrustee/wiretrustee/management/server"
)
func main() {
oldDir := flag.String("oldDir", "old store directory", "/var/wiretrustee/datadir")
newDir := flag.String("newDir", "new store directory", "/var/wiretrustee/newdatadir")
flag.Parse()
oldStore, err := server.NewStore(*oldDir)
if err != nil {
panic(err)
}
newStore, err := server.NewStore(*newDir)
if err != nil {
panic(err)
}
err = Convert(oldStore, newStore)
if err != nil {
panic(err)
}
fmt.Println("successfully converted")
}
// Convert converts old store ato a new store
// Previously Account.Id was an Auth0 user id
// Conversion moved user id to Account.CreatedBy and generated a new Account.Id using xid
// It also adds a User with id = old Account.Id with a role Admin
func Convert(oldStore *server.FileStore, newStore *server.FileStore) error {
for _, account := range oldStore.Accounts {
accountCopy := account.Copy()
accountCopy.Id = xid.New().String()
accountCopy.CreatedBy = account.Id
accountCopy.Users[account.Id] = &server.User{
Id: account.Id,
Role: server.UserRoleAdmin,
}
err := newStore.SaveAccount(accountCopy)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,76 @@
package main
import (
"github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/util"
"path/filepath"
"testing"
)
func TestConvertAccounts(t *testing.T) {
storeDir := t.TempDir()
err := util.CopyFileContents("../testdata/storev1.json", filepath.Join(storeDir, "store.json"))
if err != nil {
t.Fatal(err)
}
store, err := server.NewStore(storeDir)
if err != nil {
t.Fatal(err)
}
convertedStore, err := server.NewStore(filepath.Join(storeDir, "converted"))
if err != nil {
t.Fatal(err)
}
err = Convert(store, convertedStore)
if err != nil {
t.Fatal(err)
}
if len(store.Accounts) != len(convertedStore.Accounts) {
t.Errorf("expecting the same number of accounts after conversion")
}
for _, account := range store.Accounts {
convertedAccount, err := convertedStore.GetUserAccount(account.Id)
if err != nil || convertedAccount == nil {
t.Errorf("expecting Account %s to be converted", account.Id)
return
}
if convertedAccount.CreatedBy != account.Id {
t.Errorf("expecting converted Account.CreatedBy field to be equal to the old Account.Id")
return
}
if convertedAccount.Id == account.Id {
t.Errorf("expecting converted Account.Id to be different from Account.Id")
return
}
if len(convertedAccount.Users) != 1 {
t.Errorf("expecting converted Account.Users to be of size 1")
return
}
user := convertedAccount.Users[account.Id]
if user == nil {
t.Errorf("expecting to find a user in converted Account.Users")
return
}
if user.Role != server.UserRoleAdmin {
t.Errorf("expecting to find a user in converted Account.Users with a role Admin")
return
}
for peerId := range account.Peers {
convertedPeer := convertedAccount.Peers[peerId]
if convertedPeer == nil {
t.Errorf("expecting Account Peer of StoreV1 to be found in StoreV2")
return
}
}
}
}

View File

@@ -17,6 +17,14 @@ type Network struct {
Dns string
}
func (n *Network) Copy() *Network {
return &Network{
Id: n.Id,
Net: n.Net,
Dns: n.Dns,
}
}
// AllocatePeerIP pics an available IP from an net.IPNet.
// This method considers already taken IPs and reuses IPs if there are gaps in takenIps
// E.g. if ipNet=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.5] then the result would be 100.30.0.2

View File

@@ -206,7 +206,6 @@ func (am *AccountManager) GetPeersForAPeer(peerKey string) ([]*Peer, error) {
// Each Account has a list of pre-authorised SetupKey and if no Account has a given key err wit ha code codes.Unauthenticated
// will be returned, meaning the key is invalid
// Each new Peer will be assigned a new next net.IP from the Account.Network and Account.Network.LastIP will be updated (IP's are not reused).
// If the specified setupKey is empty then a new Account will be created //todo remove this part
// The peer property is just a placeholder for the Peer properties to pass further
func (am *AccountManager) AddPeer(setupKey string, peer Peer) (*Peer, error) {
am.mux.Lock()
@@ -218,8 +217,8 @@ func (am *AccountManager) AddPeer(setupKey string, peer Peer) (*Peer, error) {
var err error
var sk *SetupKey
if len(upperKey) == 0 {
// Empty setup key, create a new account for it.
account, sk = newAccount()
// Empty setup key, fail
return nil, status.Errorf(codes.InvalidArgument, "empty setupKey %s", setupKey)
} else {
account, err = am.Store.GetAccountBySetupKey(upperKey)
if err != nil {

View File

@@ -5,6 +5,7 @@ type Store interface {
DeletePeer(accountId string, peerKey string) (*Peer, error)
SavePeer(accountId string, peer *Peer) error
GetAccount(accountId string) (*Account, error)
GetUserAccount(userId string) (*Account, error)
GetAccountPeers(accountId string) ([]*Peer, error)
GetPeerAccount(peerKey string) (*Account, error)
GetAccountBySetupKey(setupKey string) (*Account, error)

View File

@@ -22,7 +22,17 @@
},
"Dns": null
},
"Peers": {}
"Peers": {},
"Users": {
"edafee4e-63fb-11ec-90d6-0242ac120003": {
"Id": "edafee4e-63fb-11ec-90d6-0242ac120003",
"Role": "admin"
},
"f4f6d672-63fb-11ec-90d6-0242ac120003": {
"Id": "f4f6d672-63fb-11ec-90d6-0242ac120003",
"Role": "user"
}
}
}
}
}

154
management/server/testdata/storev1.json vendored Normal file
View File

@@ -0,0 +1,154 @@
{
"Accounts": {
"auth0|61bf82ddeab084006aa1bccd": {
"Id": "auth0|61bf82ddeab084006aa1bccd",
"SetupKeys": {
"1B2B50B0-B3E8-4B0C-A426-525EDB8481BD": {
"Id": "831727121",
"Key": "1B2B50B0-B3E8-4B0C-A426-525EDB8481BD",
"Name": "One-off key",
"Type": "one-off",
"CreatedAt": "2021-12-24T16:09:45.926075752+01:00",
"ExpiresAt": "2022-01-23T16:09:45.926075752+01:00",
"Revoked": false,
"UsedTimes": 1,
"LastUsed": "2021-12-24T16:12:45.763424077+01:00"
},
"EB51E9EB-A11F-4F6E-8E49-C982891B405A": {
"Id": "1769568301",
"Key": "EB51E9EB-A11F-4F6E-8E49-C982891B405A",
"Name": "Default key",
"Type": "reusable",
"CreatedAt": "2021-12-24T16:09:45.926073628+01:00",
"ExpiresAt": "2022-01-23T16:09:45.926073628+01:00",
"Revoked": false,
"UsedTimes": 1,
"LastUsed": "2021-12-24T16:13:06.236748538+01:00"
}
},
"Network": {
"Id": "a443c07a-5765-4a78-97fc-390d9c1d0e49",
"Net": {
"IP": "100.64.0.0",
"Mask": "/8AAAA=="
},
"Dns": ""
},
"Peers": {
"oMNaI8qWi0CyclSuwGR++SurxJyM3pQEiPEHwX8IREo=": {
"Key": "oMNaI8qWi0CyclSuwGR++SurxJyM3pQEiPEHwX8IREo=",
"SetupKey": "EB51E9EB-A11F-4F6E-8E49-C982891B405A",
"IP": "100.64.0.2",
"Meta": {
"Hostname": "braginini",
"GoOS": "linux",
"Kernel": "Linux",
"Core": "21.04",
"Platform": "x86_64",
"OS": "Ubuntu",
"WtVersion": ""
},
"Name": "braginini",
"Status": {
"LastSeen": "2021-12-24T16:13:11.244342541+01:00",
"Connected": false
}
},
"xlx9/9D8+ibnRiIIB8nHGMxGOzxV17r8ShPHgi4aYSM=": {
"Key": "xlx9/9D8+ibnRiIIB8nHGMxGOzxV17r8ShPHgi4aYSM=",
"SetupKey": "1B2B50B0-B3E8-4B0C-A426-525EDB8481BD",
"IP": "100.64.0.1",
"Meta": {
"Hostname": "braginini",
"GoOS": "linux",
"Kernel": "Linux",
"Core": "21.04",
"Platform": "x86_64",
"OS": "Ubuntu",
"WtVersion": ""
},
"Name": "braginini",
"Status": {
"LastSeen": "2021-12-24T16:12:49.089339333+01:00",
"Connected": false
}
}
}
},
"google-oauth2|103201118415301331038": {
"Id": "google-oauth2|103201118415301331038",
"SetupKeys": {
"5AFB60DB-61F2-4251-8E11-494847EE88E9": {
"Id": "2485964613",
"Key": "5AFB60DB-61F2-4251-8E11-494847EE88E9",
"Name": "Default key",
"Type": "reusable",
"CreatedAt": "2021-12-24T16:10:02.238476+01:00",
"ExpiresAt": "2022-01-23T16:10:02.238476+01:00",
"Revoked": false,
"UsedTimes": 1,
"LastUsed": "2021-12-24T16:12:05.994307717+01:00"
},
"A72E4DC2-00DE-4542-8A24-62945438104E": {
"Id": "3504804807",
"Key": "A72E4DC2-00DE-4542-8A24-62945438104E",
"Name": "One-off key",
"Type": "one-off",
"CreatedAt": "2021-12-24T16:10:02.238478209+01:00",
"ExpiresAt": "2022-01-23T16:10:02.238478209+01:00",
"Revoked": false,
"UsedTimes": 1,
"LastUsed": "2021-12-24T16:11:27.015741738+01:00"
}
},
"Network": {
"Id": "b6d0b152-364e-40c1-a8a1-fa7bcac2267f",
"Net": {
"IP": "100.64.0.0",
"Mask": "/8AAAA=="
},
"Dns": ""
},
"Peers": {
"6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=": {
"Key": "6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=",
"SetupKey": "5AFB60DB-61F2-4251-8E11-494847EE88E9",
"IP": "100.64.0.2",
"Meta": {
"Hostname": "braginini",
"GoOS": "linux",
"Kernel": "Linux",
"Core": "21.04",
"Platform": "x86_64",
"OS": "Ubuntu",
"WtVersion": ""
},
"Name": "braginini",
"Status": {
"LastSeen": "2021-12-24T16:12:05.994305438+01:00",
"Connected": false
}
},
"Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=": {
"Key": "Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=",
"SetupKey": "A72E4DC2-00DE-4542-8A24-62945438104E",
"IP": "100.64.0.1",
"Meta": {
"Hostname": "braginini",
"GoOS": "linux",
"Kernel": "Linux",
"Core": "21.04",
"Platform": "x86_64",
"OS": "Ubuntu",
"WtVersion": ""
},
"Name": "braginini",
"Status": {
"LastSeen": "2021-12-24T16:11:27.015739803+01:00",
"Connected": false
}
}
}
}
}
}

71
management/server/user.go Normal file
View File

@@ -0,0 +1,71 @@
package server
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
UserRoleAdmin UserRole = "admin"
UserRoleUser UserRole = "user"
)
// UserRole is the role of the User
type UserRole string
// User represents a user of the system
type User struct {
Id string
Role UserRole
}
func (u *User) Copy() *User {
return &User{
Id: u.Id,
Role: u.Role,
}
}
// NewUser creates a new user
func NewUser(id string, role UserRole) *User {
return &User{
Id: id,
Role: role,
}
}
// NewAdminUser creates a new user with role UserRoleAdmin
func NewAdminUser(id string) *User {
return NewUser(id, UserRoleAdmin)
}
// GetOrCreateAccountByUser returns an existing account for a given user id or creates a new one if doesn't exist
func (am *AccountManager) GetOrCreateAccountByUser(userId string) (*Account, error) {
am.mux.Lock()
defer am.mux.Unlock()
account, err := am.Store.GetUserAccount(userId)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
account, _ = newAccount(userId)
account.Users[userId] = NewAdminUser(userId)
err = am.Store.SaveAccount(account)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed creating account")
}
} else {
// other error
return nil, err
}
}
return account, nil
}
// GetAccountByUser returns an existing account for a given user id, NotFound if account couldn't be found
func (am *AccountManager) GetAccountByUser(userId string) (*Account, error) {
am.mux.Lock()
defer am.mux.Unlock()
return am.Store.GetUserAccount(userId)
}

View File

@@ -12,30 +12,27 @@ fi
cleanInstall() {
printf "\033[32m Post Install of an clean install\033[0m\n"
# Step 3 (clean install), enable the service in the proper way for this platform
if [ "${use_systemctl}" = "True" ]; then
printf "\033[32m Reload the service unit from disk\033[0m\n"
systemctl daemon-reload ||:
printf "\033[32m Unmask the service\033[0m\n"
systemctl unmask wiretrustee ||:
printf "\033[32m Set the preset flag for the service unit\033[0m\n"
systemctl preset wiretrustee ||:
printf "\033[32m Set the enabled flag for the service unit\033[0m\n"
systemctl enable wiretrustee ||:
systemctl restart wiretrustee ||:
fi
/usr/local/bin/wiretrustee service install
/usr/local/bin/wiretrustee service start
}
upgrade() {
printf "\033[32m Post Install of an upgrade\033[0m\n"
if [ "${use_systemctl}" = "True" ]; then
printf "\033[32m Reload the service unit from disk\033[0m\n"
systemctl daemon-reload ||:
printf "\033[32m Restarting the service\033[0m\n"
systemctl restart wiretrustee ||:
printf "\033[32m Stopping the service\033[0m\n"
systemctl stop wiretrustee 2> /dev/null || true
fi
if [ -e /lib/systemd/system/wiretrustee.service ]; then
rm -f /lib/systemd/system/wiretrustee.service
systemctl daemon-reload
fi
# will trow an error until everyone upgrade
/usr/local/bin/wiretrustee service uninstall 2> /dev/null || true
/usr/local/bin/wiretrustee service install
/usr/local/bin/wiretrustee service start
}
# Step 2, check if this is a clean install or an upgrade
# Check if this is a clean install or an upgrade
action="$1"
if [ "$1" = "configure" ] && [ -z "$2" ]; then
# Alpine linux does not pass args, and deb passes $1=configure
@@ -50,12 +47,9 @@ case "$action" in
cleanInstall
;;
"2" | "upgrade")
printf "\033[32m Post Install of an upgrade\033[0m\n"
upgrade
;;
*)
# $1 == version being installed
printf "\033[32m install\033[0m"
cleanInstall
;;
esac

View File

@@ -0,0 +1,43 @@
#!/bin/sh
# decide if we should use systemd or init/upstart
use_systemctl="True"
systemd_version=0
if ! command -V systemctl >/dev/null 2>&1; then
use_systemctl="False"
else
systemd_version=$(systemctl --version | head -1 | sed 's/systemd //g')
fi
remove() {
printf "\033[32m Pre uninstall\033[0m\n"
if [ "${use_systemctl}" = "True" ]; then
printf "\033[32m Stopping the service\033[0m\n"
systemctl stop wiretrustee || true
if [ -e /lib/systemd/system/wiretrustee.service ]; then
rm -f /lib/systemd/system/wiretrustee.service
systemctl daemon-reload || true
fi
fi
printf "\033[32m Uninstalling the service\033[0m\n"
/usr/local/bin/wiretrustee service uninstall || true
if [ "${use_systemctl}" = "True" ]; then
printf "\n\033[32m running daemon reload\033[0m\n"
systemctl daemon-reload || true
fi
}
action="$1"
case "$action" in
"0" | "remove")
remove
;;
*)
exit 0
;;
esac

View File

@@ -1,30 +0,0 @@
{
"PrivateKey": "",
"Peers": [
{
"WgPubKey": "",
"WgAllowedIps": ""
}
],
"StunTurnURLs": [
{
"Scheme": 1,
"Host": "",
"Port": 3468,
"Username": "",
"Password": "",
"Proto": 1
},
{
"Scheme": 3,
"Host": "",
"Port": 3468,
"Username": "",
"Password": "",
"Proto": 1
}
],
"SignalAddr": "",
"WgAddr": "",
"WgIface": ""
}

View File

@@ -1,10 +0,0 @@
[Unit]
Description=Wiretrustee Service
After=multi-user.target network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/wiretrustee up --config /etc/wiretrustee/config.json --log-level debug
[Install]
WantedBy=multi-user.target

View File

@@ -11,7 +11,9 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
@@ -23,6 +25,12 @@ import (
// A set of tools to exchange connection details (Wireguard endpoints) with the remote peer.
// Status is the status of the client
type Status string
const StreamConnected Status = "Connected"
const StreamDisconnected Status = "Disconnected"
// Client Wraps the Signal Exchange Service gRpc client
type Client struct {
key wgtypes.Key
@@ -30,8 +38,15 @@ type Client struct {
signalConn *grpc.ClientConn
ctx context.Context
stream proto.SignalExchange_ConnectStreamClient
//waiting group to notify once stream is connected
connWg *sync.WaitGroup //todo use a channel instead??
// connectedCh used to notify goroutines waiting for the connection to the Signal stream
connectedCh chan struct{}
mux sync.Mutex
// StreamConnected indicates whether this client is StreamConnected to the Signal stream
status Status
}
func (c *Client) GetStatus() Status {
return c.status
}
// Close Closes underlying connections to the Signal Exchange
@@ -42,13 +57,13 @@ func (c *Client) Close() error {
// NewClient creates a new Signal client
func NewClient(ctx context.Context, addr string, key wgtypes.Key, tlsEnabled bool) (*Client, error) {
transportOption := grpc.WithInsecure()
transportOption := grpc.WithTransportCredentials(insecure.NewCredentials())
if tlsEnabled {
transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))
}
sigCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
sigCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
conn, err := grpc.DialContext(
sigCtx,
@@ -56,8 +71,8 @@ func NewClient(ctx context.Context, addr string, key wgtypes.Key, tlsEnabled boo
transportOption,
grpc.WithBlock(),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 3 * time.Second,
Timeout: 2 * time.Second,
Time: 15 * time.Second,
Timeout: 10 * time.Second,
}))
if err != nil {
@@ -65,60 +80,105 @@ func NewClient(ctx context.Context, addr string, key wgtypes.Key, tlsEnabled boo
return nil, err
}
var wg sync.WaitGroup
return &Client{
realClient: proto.NewSignalExchangeClient(conn),
ctx: ctx,
signalConn: conn,
key: key,
connWg: &wg,
mux: sync.Mutex{},
status: StreamDisconnected,
}, nil
}
//defaultBackoff is a basic backoff mechanism for general issues
func defaultBackoff() backoff.BackOff {
return &backoff.ExponentialBackOff{
func defaultBackoff(ctx context.Context) backoff.BackOff {
return backoff.WithContext(&backoff.ExponentialBackOff{
InitialInterval: 800 * time.Millisecond,
RandomizationFactor: backoff.DefaultRandomizationFactor,
Multiplier: backoff.DefaultMultiplier,
MaxInterval: 30 * time.Second,
MaxElapsedTime: 24 * 3 * time.Hour, //stop after 3 days trying
MaxInterval: 10 * time.Second,
MaxElapsedTime: 12 * time.Hour, //stop after 12 hours of trying, the error will be propagated to the general retry of the client
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}
}, ctx)
}
// Receive Connects to the Signal Exchange message stream and starts receiving messages.
// The messages will be handled by msgHandler function provided.
// This function runs a goroutine underneath and reconnects to the Signal Exchange if errors occur (e.g. Exchange restart)
// The key is the identifier of our Peer (could be Wireguard public key)
func (c *Client) Receive(msgHandler func(msg *proto.Message) error) {
c.connWg.Add(1)
go func() {
// This function is blocking and reconnects to the Signal Exchange if errors occur (e.g. Exchange restart)
// The connection retry logic will try to reconnect for 30 min and if wasn't successful will propagate the error to the function caller.
func (c *Client) Receive(msgHandler func(msg *proto.Message) error) error {
var backOff = defaultBackoff()
var backOff = defaultBackoff(c.ctx)
operation := func() error {
err := c.connect(c.key.PublicKey().String(), msgHandler)
if err != nil {
log.Warnf("disconnected from the Signal Exchange due to an error %s. Retrying ... ", err)
c.connWg.Add(1)
return err
}
operation := func() error {
backOff.Reset()
return nil
c.notifyStreamDisconnected()
log.Debugf("signal connection state %v", c.signalConn.GetState())
if !c.ready() {
return fmt.Errorf("no connection to signal")
}
err := backoff.Retry(operation, backOff)
// connect to Signal stream identifying ourselves with a public Wireguard key
// todo once the key rotation logic has been implemented, consider changing to some other identifier (received from management)
stream, err := c.connect(c.key.PublicKey().String())
if err != nil {
log.Errorf("error while communicating with the Signal Exchange %s ", err)
return
log.Warnf("disconnected from the Signal Exchange due to an error: %v", err)
return err
}
}()
c.notifyStreamConnected()
log.Infof("connected to the Signal Service stream")
// start receiving messages from the Signal stream (from other peers through signal)
err = c.receive(stream, msgHandler)
if err != nil {
log.Warnf("disconnected from the Signal Exchange due to an error: %v", err)
backOff.Reset()
return err
}
return nil
}
err := backoff.Retry(operation, backOff)
if err != nil {
log.Errorf("exiting Signal Service connection retry loop due to unrecoverable error: %s", err)
return err
}
return nil
}
func (c *Client) notifyStreamDisconnected() {
c.mux.Lock()
defer c.mux.Unlock()
c.status = StreamDisconnected
}
func (c *Client) connect(key string, msgHandler func(msg *proto.Message) error) error {
func (c *Client) notifyStreamConnected() {
c.mux.Lock()
defer c.mux.Unlock()
c.status = StreamConnected
if c.connectedCh != nil {
// there are goroutines waiting on this channel -> release them
close(c.connectedCh)
c.connectedCh = nil
}
}
func (c *Client) getStreamStatusChan() <-chan struct{} {
c.mux.Lock()
defer c.mux.Unlock()
if c.connectedCh == nil {
c.connectedCh = make(chan struct{})
}
return c.connectedCh
}
func (c *Client) connect(key string) (proto.SignalExchange_ConnectStreamClient, error) {
c.stream = nil
// add key fingerprint to the request header to be identified on the server side
@@ -129,35 +189,48 @@ func (c *Client) connect(key string, msgHandler func(msg *proto.Message) error)
c.stream = stream
if err != nil {
return err
return nil, err
}
// blocks
header, err := c.stream.Header()
if err != nil {
return err
return nil, err
}
registered := header.Get(proto.HeaderRegistered)
if len(registered) == 0 {
return fmt.Errorf("didn't receive a registration header from the Signal server whille connecting to the streams")
return nil, fmt.Errorf("didn't receive a registration header from the Signal server whille connecting to the streams")
}
//connection established we are good to use the stream
c.connWg.Done()
log.Infof("connected to the Signal Exchange Stream")
return c.receive(stream, msgHandler)
return stream, nil
}
// WaitConnected waits until the client is connected to the message stream
func (c *Client) WaitConnected() {
c.connWg.Wait()
// ready indicates whether the client is okay and ready to be used
// for now it just checks whether gRPC connection to the service is in state Ready
func (c *Client) ready() bool {
return c.signalConn.GetState() == connectivity.Ready
}
// WaitStreamConnected waits until the client is connected to the Signal stream
func (c *Client) WaitStreamConnected() {
if c.status == StreamConnected {
return
}
ch := c.getStreamStatusChan()
select {
case <-c.ctx.Done():
case <-ch:
}
}
// SendToStream sends a message to the remote Peer through the Signal Exchange using established stream connection to the Signal Server
// The Client.Receive method must be called before sending messages to establish initial connection to the Signal Exchange
// Client.connWg can be used to wait
func (c *Client) SendToStream(msg *proto.EncryptedMessage) error {
if !c.ready() {
return fmt.Errorf("no connection to signal")
}
if c.stream == nil {
return fmt.Errorf("connection to the Signal Exchnage has not been established yet. Please call Client.Receive before sending messages")
}
@@ -214,13 +287,17 @@ func (c *Client) encryptMessage(msg *proto.Message) (*proto.EncryptedMessage, er
// Send sends a message to the remote Peer through the Signal Exchange.
func (c *Client) Send(msg *proto.Message) error {
if !c.ready() {
return fmt.Errorf("no connection to signal")
}
encryptedMessage, err := c.encryptMessage(msg)
if err != nil {
return err
}
_, err = c.realClient.Send(context.TODO(), encryptedMessage)
if err != nil {
log.Errorf("error while sending message to peer [%s] [error: %v]", msg.RemoteKey, err)
//log.Errorf("error while sending message to peer [%s] [error: %v]", msg.RemoteKey, err)
return err
}
@@ -237,10 +314,10 @@ func (c *Client) receive(stream proto.SignalExchange_ConnectStreamClient,
log.Warnf("stream canceled (usually indicates shutdown)")
return err
} else if s.Code() == codes.Unavailable {
log.Warnf("server has been stopped")
log.Warnf("Signal Service is unavailable")
return err
} else if err == io.EOF {
log.Warnf("stream closed by server")
log.Warnf("Signal Service stream closed by server")
return err
} else if err != nil {
return err

View File

@@ -9,6 +9,7 @@ import (
"github.com/wiretrustee/wiretrustee/signal/server"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/metadata"
"net"
@@ -48,30 +49,42 @@ var _ = Describe("Client", func() {
// connect PeerA to Signal
keyA, _ := wgtypes.GenerateKey()
clientA := createSignalClient(addr, keyA)
clientA.Receive(func(msg *sigProto.Message) error {
receivedOnA = msg.GetBody().GetPayload()
msgReceived.Done()
return nil
})
clientA.WaitConnected()
go func() {
err := clientA.Receive(func(msg *sigProto.Message) error {
receivedOnA = msg.GetBody().GetPayload()
msgReceived.Done()
return nil
})
if err != nil {
return
}
}()
clientA.WaitStreamConnected()
// connect PeerB to Signal
keyB, _ := wgtypes.GenerateKey()
clientB := createSignalClient(addr, keyB)
clientB.Receive(func(msg *sigProto.Message) error {
receivedOnB = msg.GetBody().GetPayload()
err := clientB.Send(&sigProto.Message{
Key: keyB.PublicKey().String(),
RemoteKey: keyA.PublicKey().String(),
Body: &sigProto.Body{Payload: "pong"},
go func() {
err := clientB.Receive(func(msg *sigProto.Message) error {
receivedOnB = msg.GetBody().GetPayload()
err := clientB.Send(&sigProto.Message{
Key: keyB.PublicKey().String(),
RemoteKey: keyA.PublicKey().String(),
Body: &sigProto.Body{Payload: "pong"},
})
if err != nil {
Fail("failed sending a message to PeerA")
}
msgReceived.Done()
return nil
})
if err != nil {
Fail("failed sending a message to PeerA")
return
}
msgReceived.Done()
return nil
})
clientB.WaitConnected()
}()
clientB.WaitStreamConnected()
// PeerA initiates ping-pong
err := clientA.Send(&sigProto.Message{
@@ -100,11 +113,15 @@ var _ = Describe("Client", func() {
key, _ := wgtypes.GenerateKey()
client := createSignalClient(addr, key)
client.Receive(func(msg *sigProto.Message) error {
return nil
})
client.WaitConnected()
go func() {
err := client.Receive(func(msg *sigProto.Message) error {
return nil
})
if err != nil {
return
}
}()
client.WaitStreamConnected()
Expect(client).NotTo(BeNil())
})
})
@@ -154,7 +171,8 @@ func createSignalClient(addr string, key wgtypes.Key) *Client {
func createRawSignalClient(addr string) sigProto.SignalExchangeClient {
ctx := context.Background()
conn, err := grpc.DialContext(ctx, addr, grpc.WithInsecure(),
conn, err := grpc.DialContext(ctx, addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 3 * time.Second,