Compare commits

...

24 Commits

Author SHA1 Message Date
Misha Bragin
5c84ba73fb Revert "Add UI binary to windows installer (#283)"
This reverts commit 9ec4ea8e03.
2022-03-23 09:15:06 +01:00
Givi Khojanashvili
9ec4ea8e03 Add UI binary to windows installer (#283)
Windows installer has been updated to add
systray UI as an installation step additionally
to the service installation
2022-03-23 09:13:57 +01:00
braginini
9c3cd1a5db Add basic desktop UI - systray 2022-03-08 16:14:00 +01:00
braginini
347a668bd5 Fix UP cmd to pass managementURL to daemon 2022-03-08 16:10:44 +01:00
Givi Khojanashvili
ef47385e38 Split client app into cmd and daemon service (#239) 2022-03-08 14:47:55 +01:00
Mikhail Bragin
3e46f38166 Fix README 2022-03-06 21:40:09 +01:00
Mikhail Bragin
64e2e34dae Add Slack badge 2022-03-06 14:16:17 +01:00
Mikhail Bragin
8dd92f14bf Add more badges to README
Codacy and Go report badges
2022-03-06 09:57:07 +01:00
Maycon Santos
071b03e790 Updated self-hosted scripts and documentation (#249)
* Updated self-hosted scripts and documentation

Added more variables to setup.env and
Updated the documentation.

We are now configuring turn server
with template as well.

* Updated self-hosted scripts and documentation

Added more variables to setup.env and
Updated the documentation.

We are now configuring turn server
with template as well.

* Updated self-hosted scripts and documentation

Added more variables to setup.env and
Updated the documentation.

We are now configuring turn server
with template as well.

* Updated self-hosted scripts and documentation

Added more variables to setup.env and
Updated the documentation.

We are now configuring turn server
with template as well.
2022-03-05 11:20:04 +01:00
Maycon Santos
3385ea6379 Update windows sign pipeline version (#248)
With the latest versions of runner 
and updates of dependencies 
a new version was generated.
2022-03-03 10:21:58 +01:00
Mikhail Bragin
430e0415df Remove Wireguard peer in no-proxy mode on Close (#247)
When connection is closed the wireguard peer should be gone.
It wasn't happening until this fix.
2022-03-02 14:50:22 +01:00
Mikhail Bragin
b72ed91cb4 Update self-hosting docs
Running Dashboard, Management, Signal, and Coturn section.
Mentioned in #244
2022-03-02 09:52:09 +01:00
Maycon Santos
0b8387bd2c Group users of same private domain (#243)
* Added Domain Category field and fix store tests

* Add GetAccountByDomain method

* Add Domain Category to authorization claims

* Initial GetAccountWithAuthorizationClaims test cases

* Renamed Private Domain map and index it on saving account

* New Go build tags

* Added NewRegularUser function

* Updated restore to account for primary domain account

Also, added another test case

* Added grouping user of private domains

Also added auxiliary methods for update metadata and domain attributes

* Update http handles get account method and tests

* Fix lint and document another case

* Removed unnecessary log

* Move use cases to method and add flow comments

* Split the new user and existing logic from GetAccountWithAuthorizationClaims

* Review: minor corrections

Co-authored-by: braginini <bangvalo@gmail.com>
2022-03-01 15:22:18 +01:00
Mikhail Bragin
5d4c2643a3 Support no-proxy mode connection mode (#245)
When one of the peers has a static public host IP
or both peers are in the same local network
we establish a direct Wireguard connection
bypassing proxy usage.
This helps reduce FD usage and improves
performance.
2022-03-01 14:07:33 +01:00
Mikhail Bragin
69cda73bbb Update README badges 2022-02-28 16:51:12 +01:00
Maycon Santos
b29948b910 Jwtclaims package (#242)
* Move JWTClaims logic to its own package

* Add extractor tests
2022-02-23 20:02:02 +01:00
shatoboar
5f5cbf7e20 Test mgmt http handler (#240) 2022-02-22 18:18:05 +01:00
shatoboar
41c6af6b6f Extracted AccountManager to interface (#230) 2022-02-22 11:28:19 +01:00
Mikhail Bragin
23fad49756 Update docker pull badge 2022-02-22 11:24:24 +01:00
Maycon Santos
5546eba36a Write to temp file before saving data (#238)
* Create temp file before saving data

On the event of full disk, we may encounter the case where the
destination file get replaced by an empty file as the
ioutil.WriteFile truncates the destination before write.

* Close the tempFile instance before moving it

* Blacklist Wireguard interfaces for ICE checks
2022-02-20 19:03:16 +01:00
braginini
60a9da734f Bump wiretrustee-ice version 2022-02-18 15:06:44 +01:00
Maycon Santos
852c7c50c0 Fix IDPManagement initialization when no config (#234)
* initialize idpmanage only if config was set

* removed LetsEncryption information for local selfhosted deployments
2022-02-17 19:18:46 +01:00
braginini
1c2c1a876b chore: fix selected candidate pair logging 2022-02-17 08:36:37 +01:00
Mikhail Bragin
e5dcd4753e single socket ice (#232)
Enables single socket for HOST and SRFLX candidates by utilizing pion.ice UDPMux
2022-02-16 20:00:21 +01:00
66 changed files with 3481 additions and 855 deletions

View File

@@ -64,6 +64,6 @@ jobs:
with:
workflow: Sign windows bin and installer
repo: wiretrustee/windows-sign-pipeline
ref: v0.0.1
ref: v0.0.2
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref }}" }'
inputs: '{ "tag": "${{ github.ref }}" }'

View File

@@ -5,12 +5,26 @@
</p>
<p>
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
<img src="https://img.shields.io/docker/pulls/wiretrustee/management" />
<a href="https://github.com/wiretrustee/wiretrustee/blob/main/LICENSE">
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
</a>
<a href="https://hub.docker.com/r/wiretrustee/wiretrustee/tags">
<img src="https://img.shields.io/docker/pulls/wiretrustee/wiretrustee" />
</a>
<img src="https://badgen.net/badge/Open%20Source%3F/Yes%21/blue?icon=github" />
<br>
<a href="https://www.codacy.com/gh/wiretrustee/wiretrustee/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=wiretrustee/wiretrustee&amp;utm_campaign=Badge_Grade"><img src="https://app.codacy.com/project/badge/Grade/d366de2c9d8b4cf982da27f8f5831809"/></a>
<a href="https://goreportcard.com/report/wiretrustee/wiretrustee">
<img src="https://goreportcard.com/badge/github.com/wiretrustee/wiretrustee?style=flat-square" />
</a>
<br>
<a href="https://join.slack.com/t/wiretrustee/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">
<img src="https://img.shields.io/badge/slack-@wiretrustee-red.svg?logo=slack"/>
</a>
</p>
</div>
<p align="center">
<strong>
Start using Wiretrustee at <a href="https://app.wiretrustee.com/">app.wiretrustee.com</a>
@@ -179,46 +193,7 @@ For **Windows** systems:
3. Repeat on other machines.
### Running Dashboard, Management, Signal and Coturn
Wiretrustee uses [Auth0](https://auth0.com) for user authentication and authorization, therefore you will need to create a free account
and configure Auth0 variables in the compose file (dashboard) and in the management config file.
We chose Auth0 to "outsource" the user management part of our platform because we believe that implementing a proper user auth is not a trivial task and requires a significant amount of time to make it right. We focused on connectivity instead.
It is worth mentioning that the dependency on Auth0 is the only one that cannot be self-hosted.
Configuring Wiretrustee Auth0 integration:
- check [How to run](https://github.com/wiretrustee/wiretrustee-dashboard#how-to-run) to obtain Auth0 environment variables for UI Dashboard
- set these variables in the [environment section of the docker-compose file](https://github.com/wiretrustee/wiretrustee/blob/main/infrastructure_files/docker-compose.yml)
- check [Auth0 Golang API Guide](https://auth0.com/docs/quickstart/backend/golang) to obtain ```AuthIssuer```, ```AuthAudience```, and ```AuthKeysLocation```
- set these properties in the [management config files](https://github.com/wiretrustee/wiretrustee/blob/main/infrastructure_files/management.json#L33)
Under infrastructure_files we have a docker-compose example to run Dashboard, Wiretrustee Management and Signal services, plus an instance of [Coturn](https://github.com/coturn/coturn), it also provides a turnserver.conf file as a simple example of Coturn configuration.
You can edit the turnserver.conf file and change its Realm setting (defaults to wiretrustee.com) to your own domain and user setting (defaults to username1:password1) to **proper credentials**.
The example is set to use the official images from Wiretrustee and Coturn; you can find our documentation to run the signal server in docker in [Running the Signal service](#running-the-signal-service), the management in [Management](./management/README.md), and the Coturn official documentation [here](https://hub.docker.com/r/coturn/coturn).
> Run Coturn at your own risk, we are just providing an example, be sure to follow security best practices and to configure proper credentials as this service can be exploited and you may face large data transfer charges.
Also, if you have an SSL certificate for Coturn, you can modify the docker-compose.yml file to point to its files in your host machine and then switch the domain name to your own SSL domain. If you don't already have an SSL certificate, you can follow [Certbot's](https://certbot.eff.org/docs/intro.html) official documentation
to generate one from [Lets Encrypt](https://letsencrypt.org/), or, we found that the example provided by [BigBlueButton](https://docs.bigbluebutton.org/2.2/setup-turn-server.html#generating-tls-certificates) covers the basics to configure Coturn with Let's Encrypt certs.
> The Wiretrustee Management service can generate and maintain the certificates automatically, all you need to do is run the service in a host with a public IP, configure a valid DNS record pointing to that IP and uncomment the 443 ports and command lines in the docker-compose.yml file.
Simple docker-composer execution:
````shell
cd infrastructure_files
docker-compose up -d
````
You can check logs by running:
````shell
cd infrastructure_files
docker-compose logs signal
docker-compose logs management
docker-compose logs coturn
````
If you need to stop the services, run the following:
````shell
cd infrastructure_files
docker-compose down
````
See [Self-Hosting Guide](https://docs.wiretrustee.com/getting-started/self-hosting)
### Legal

37
client/cmd/down.go Normal file
View File

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

View File

@@ -1,175 +1,66 @@
package cmd
import (
"bufio"
"context"
"fmt"
"os"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/wiretrustee/wiretrustee/client/internal"
"github.com/wiretrustee/wiretrustee/client/system"
mgm "github.com/wiretrustee/wiretrustee/management/client"
mgmProto "github.com/wiretrustee/wiretrustee/management/proto"
"github.com/wiretrustee/wiretrustee/util"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/wiretrustee/wiretrustee/client/proto"
)
var (
loginCmd = &cobra.Command{
Use: "login",
Short: "login to the Wiretrustee Management Service (first run)",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
var loginCmd = &cobra.Command{
Use: "login",
Short: "login to the Wiretrustee Management Service (first run)",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
ctx := internal.CtxInitState(context.Background())
var backOff = &backoff.ExponentialBackOff{
InitialInterval: time.Second,
RandomizationFactor: backoff.DefaultRandomizationFactor,
Multiplier: backoff.DefaultMultiplier,
MaxInterval: 2 * time.Second,
MaxElapsedTime: time.Second * 10,
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}
loginOp := func() error {
err := util.InitLog(logLevel, logFile)
if err != nil {
log.Errorf("failed initializing log %v", err)
return err
}
config, err := internal.GetConfig(managementURL, configPath, preSharedKey)
if err != nil {
log.Errorf("failed getting 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 := context.Background()
mgmTlsEnabled := false
if config.ManagementURL.Scheme == "https" {
mgmTlsEnabled = true
}
log.Debugf("connecting to Management Service %s", config.ManagementURL.String())
mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
if err != nil {
log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), err)
return err
}
log.Debugf("connected to management Service %s", config.ManagementURL.String())
serverKey, err := mgmClient.GetServerPublicKey()
if err != nil {
log.Errorf("failed while getting Management Service public key: %v", err)
return err
}
_, err = loginPeer(*serverKey, mgmClient, setupKey)
if err != nil {
log.Errorf("failed logging-in peer on Management Service : %v", err)
return err
}
err = mgmClient.Close()
if err != nil {
log.Errorf("failed closing Management Service client: %v", err)
return err
}
return nil
}
err := backoff.RetryNotify(loginOp, backOff, func(err error, duration time.Duration) {
log.Warnf("retrying Login to the Management service in %v due to error %v", duration, err)
})
// workaround to run without service
if logFile == "console" {
config, err := internal.GetConfig(managementURL, configPath, preSharedKey)
if err != nil {
log.Errorf("exiting login retry loop due to unrecoverable error: %v", err)
log.Errorf("get config file: %v", err)
return err
}
return nil
},
}
)
// loginPeer attempts to login to Management Service. If peer wasn't registered, tries the registration flow.
func loginPeer(serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string) (*mgmProto.LoginResponse, error) {
loginResp, err := client.Login(serverPublicKey)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
log.Debugf("peer registration required")
return registerPeer(serverPublicKey, client, setupKey)
} else {
return nil, err
err = WithBackOff(func() error {
return internal.Login(ctx, config, setupKey)
})
if err != nil {
log.Errorf("backoff cycle failed: %v", err)
}
return err
}
}
log.Info("peer has successfully logged-in to Management Service")
if setupKey == "" {
log.Error("setup key can't be empty")
return fmt.Errorf("empty setup key")
}
return loginResp, nil
}
// registerPeer checks whether setupKey was provided via cmd line and if not then it prompts user to enter a key.
// Otherwise tries to register with the provided setupKey via command line.
func registerPeer(serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string) (*mgmProto.LoginResponse, error) {
var err error
if setupKey == "" {
setupKey, err = promptPeerSetupKey()
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
log.Errorf("failed getting setup key from user: %s", err)
return nil, err
log.Errorf("failed to connect to service CLI interface %v", err)
return err
}
}
defer conn.Close()
validSetupKey, err := uuid.Parse(setupKey)
if err != nil {
return nil, err
}
log.Debugf("sending peer registration request to Management Service")
info := system.GetInfo()
loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), info)
if err != nil {
log.Errorf("failed registering peer %v", err)
return nil, err
}
log.Infof("peer has been successfully registered on Management Service")
return loginResp, nil
}
// promptPeerSetupKey prompts user to enter Setup Key
func promptPeerSetupKey() (string, error) {
fmt.Print("Enter setup key: ")
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
input := s.Text()
if input != "" {
return input, nil
request := proto.LoginRequest{
SetupKey: setupKey,
PresharedKey: preSharedKey,
ManagementUrl: managementURL,
}
fmt.Println("Specified key is empty, try again:")
}
return "", s.Err()
client := proto.NewDaemonServiceClient(conn)
err = WithBackOff(func() error {
if _, err := client.Login(ctx, &request); err != nil {
log.Errorf("try login: %v", err)
}
return err
})
if err != nil {
log.Errorf("backoff cycle failed: %v", err)
}
return err
},
}

View File

@@ -2,13 +2,14 @@ package cmd
import (
"fmt"
"path/filepath"
"strings"
"testing"
"github.com/wiretrustee/wiretrustee/client/internal"
"github.com/wiretrustee/wiretrustee/iface"
mgmt "github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/util"
"path/filepath"
"strings"
"testing"
)
var mgmAddr string
@@ -25,12 +26,11 @@ func TestLogin_Start(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, listener := startManagement(config, t)
_, listener := startManagement(t, config)
mgmAddr = listener.Addr().String()
}
func TestLogin(t *testing.T) {
tempDir := t.TempDir()
confPath := tempDir + "/config.json"
mgmtURL := fmt.Sprintf("http://%s", mgmAddr)
@@ -38,6 +38,8 @@ func TestLogin(t *testing.T) {
"login",
"--config",
confPath,
"--log-file",
"console",
"--setup-key",
strings.ToUpper("a2c8e62b-38f5-4553-b31e-dd66c696cebb"),
"--management-url",

View File

@@ -1,16 +1,23 @@
package cmd
import (
"context"
"fmt"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/wiretrustee/wiretrustee/client/internal"
"os"
"os/signal"
"runtime"
"strings"
"syscall"
"time"
"github.com/cenkalti/backoff/v4"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/wiretrustee/wiretrustee/client/internal"
)
var (
@@ -19,6 +26,7 @@ var (
logLevel string
defaultLogFile string
logFile string
daemonAddr string
managementURL string
setupKey string
preSharedKey string
@@ -37,8 +45,8 @@ var (
func Execute() error {
return rootCmd.Execute()
}
func init() {
func init() {
stopCh = make(chan int)
cleanupCh = make(chan struct{})
@@ -49,6 +57,11 @@ func init() {
defaultLogFile = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "client.log"
}
defaultDaemonAddr := "unix:///var/run/wiretrustee.sock"
if runtime.GOOS == "windows" {
defaultDaemonAddr = "tcp://127.0.0.1:41731"
}
rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
rootCmd.PersistentFlags().StringVar(&managementURL, "management-url", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.ManagementURLDefault().String()))
rootCmd.PersistentFlags().StringVar(&configPath, "config", defaultConfigPath, "Wiretrustee config file location")
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "sets Wiretrustee log level")
@@ -57,6 +70,8 @@ func init() {
rootCmd.PersistentFlags().StringVar(&preSharedKey, "preshared-key", "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.")
rootCmd.AddCommand(serviceCmd)
rootCmd.AddCommand(upCmd)
rootCmd.AddCommand(downCmd)
rootCmd.AddCommand(statusCmd)
rootCmd.AddCommand(loginCmd)
rootCmd.AddCommand(versionCmd)
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
@@ -79,7 +94,6 @@ func SetupCloseHandler() {
func SetFlagsFromEnvVars() {
flags := rootCmd.PersistentFlags()
flags.VisitAll(func(f *pflag.Flag) {
envVar := FlagNameToEnvVar(f.Name)
if value, present := os.LookupEnv(envVar); present {
@@ -99,3 +113,34 @@ func FlagNameToEnvVar(f string) string {
upper := strings.ToUpper(parsed)
return prefix + upper
}
// DialClientGRPCServer returns client connection to the dameno server.
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
return grpc.DialContext(
ctx,
strings.TrimPrefix(addr, "tcp://"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
}
// WithBackOff execute function in backoff cycle.
func WithBackOff(bf func() error) error {
return backoff.RetryNotify(bf, CLIBackOffSettings, func(err error, duration time.Duration) {
log.Warnf("retrying Login to the Management service in %v due to error %v", duration, err)
})
}
// CLIBackOffSettings is default backoff settings for CLI commands.
var CLIBackOffSettings = &backoff.ExponentialBackOff{
InitialInterval: time.Second,
RandomizationFactor: backoff.DefaultRandomizationFactor,
Multiplier: backoff.DefaultMultiplier,
MaxInterval: 10 * time.Second,
MaxElapsedTime: 30 * time.Second,
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}

View File

@@ -1,14 +1,30 @@
package cmd
import (
"context"
"github.com/kardianos/service"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"github.com/wiretrustee/wiretrustee/client/internal"
)
type program struct {
cmd *cobra.Command
args []string
ctx context.Context
cmd *cobra.Command
args []string
serv *grpc.Server
}
func newProgram(cmd *cobra.Command, args []string) *program {
ctx := internal.CtxInitState(cmd.Context())
return &program{
ctx: ctx,
cmd: cmd,
args: args,
}
}
func newSVCConfig() *service.Config {
@@ -28,9 +44,8 @@ func newSVC(prg *program, conf *service.Config) (service.Service, error) {
return s, nil
}
var (
serviceCmd = &cobra.Command{
Use: "service",
Short: "manages wiretrustee service",
}
)
var serviceCmd = &cobra.Command{
Use: "service",
Short: "manages wiretrustee service",
}

View File

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

View File

@@ -1,69 +1,67 @@
package cmd
import (
"github.com/spf13/cobra"
"runtime"
"github.com/spf13/cobra"
)
var (
installCmd = &cobra.Command{
Use: "install",
Short: "installs wiretrustee service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
var installCmd = &cobra.Command{
Use: "install",
Short: "installs wiretrustee service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
svcConfig := newSVCConfig()
svcConfig := newSVCConfig()
svcConfig.Arguments = []string{
"service",
"run",
"--config",
configPath,
"--log-level",
logLevel,
}
svcConfig.Arguments = []string{
"service",
"run",
"--config",
configPath,
"--log-level",
logLevel,
}
if runtime.GOOS == "linux" {
// Respected only by systemd systems
svcConfig.Dependencies = []string{"After=network.target syslog.target"}
}
if runtime.GOOS == "linux" {
// Respected only by systemd systems
svcConfig.Dependencies = []string{"After=network.target syslog.target"}
}
s, err := newSVC(&program{}, svcConfig)
if err != nil {
cmd.PrintErrln(err)
return err
}
s, err := newSVC(newProgram(cmd, args), svcConfig)
if err != nil {
cmd.PrintErrln(err)
return err
}
err = s.Install()
if err != nil {
cmd.PrintErrln(err)
return err
}
cmd.Println("Wiretrustee service has been installed")
return nil
},
}
)
err = s.Install()
if err != nil {
cmd.PrintErrln(err)
return err
}
cmd.Println("Wiretrustee service has been installed")
return nil
},
}
var (
uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "uninstalls wiretrustee service from system",
Run: func(cmd *cobra.Command, args []string) {
SetFlagsFromEnvVars()
var uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "uninstalls wiretrustee service from system",
Run: func(cmd *cobra.Command, args []string) {
SetFlagsFromEnvVars()
s, err := newSVC(&program{}, newSVCConfig())
if err != nil {
cmd.PrintErrln(err)
return
}
s, err := newSVC(newProgram(cmd, args), newSVCConfig())
if err != nil {
cmd.PrintErrln(err)
return
}
err = s.Uninstall()
if err != nil {
cmd.PrintErrln(err)
return
}
cmd.Println("Wiretrustee has been uninstalled")
},
}
err = s.Uninstall()
if err != nil {
cmd.PrintErrln(err)
return
}
cmd.Println("Wiretrustee has been uninstalled")
},
}
)

37
client/cmd/status.go Normal file
View File

@@ -0,0 +1,37 @@
package cmd
import (
"context"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
"github.com/wiretrustee/wiretrustee/client/internal"
"github.com/wiretrustee/wiretrustee/client/proto"
)
var statusCmd = &cobra.Command{
Use: "status",
Short: "status of the Wiretrustee Service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
ctx := internal.CtxInitState(context.Background())
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
log.Errorf("failed to connect to service CLI interface %v", err)
return err
}
defer conn.Close()
resp, err := proto.NewDaemonServiceClient(conn).Status(cmd.Context(), &proto.StatusRequest{})
if err != nil {
log.Errorf("status failed: %v", status.Convert(err).Message())
return nil
}
log.Infof("status: %v", resp.Status)
return nil
},
}

View File

@@ -1,13 +1,18 @@
package cmd
import (
"context"
"net"
"testing"
"time"
clientProto "github.com/wiretrustee/wiretrustee/client/proto"
client "github.com/wiretrustee/wiretrustee/client/server"
mgmtProto "github.com/wiretrustee/wiretrustee/management/proto"
mgmt "github.com/wiretrustee/wiretrustee/management/server"
sigProto "github.com/wiretrustee/wiretrustee/signal/proto"
sig "github.com/wiretrustee/wiretrustee/signal/server"
"google.golang.org/grpc"
"net"
"testing"
)
func startSignal(t *testing.T) (*grpc.Server, net.Listener) {
@@ -26,7 +31,7 @@ func startSignal(t *testing.T) (*grpc.Server, net.Listener) {
return s, lis
}
func startManagement(config *mgmt.Config, t *testing.T) (*grpc.Server, net.Listener) {
func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Listener) {
lis, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
@@ -48,9 +53,40 @@ func startManagement(config *mgmt.Config, t *testing.T) (*grpc.Server, net.Liste
go func() {
if err := s.Serve(lis); err != nil {
t.Error(err)
return
}
}()
return s, lis
}
func startClientDaemon(
t *testing.T, ctx context.Context, managementURL, configPath string,
stopCh chan int, cleanupCh chan<- struct{},
) (*grpc.Server, net.Listener) {
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
s := grpc.NewServer()
server := client.New(
ctx,
managementURL,
configPath,
stopCh,
cleanupCh,
)
if err := server.Start(); err != nil {
t.Fatal(err)
}
clientProto.RegisterDaemonServiceServer(s, server)
go func() {
if err := s.Serve(lis); err != nil {
t.Error(err)
}
}()
time.Sleep(time.Second)
return s, lis
}

View File

@@ -1,238 +1,78 @@
package cmd
import (
"context"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/kardianos/service"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/wiretrustee/wiretrustee/client/internal"
mgm "github.com/wiretrustee/wiretrustee/management/client"
mgmProto "github.com/wiretrustee/wiretrustee/management/proto"
signal "github.com/wiretrustee/wiretrustee/signal/client"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/wiretrustee/wiretrustee/client/proto"
)
var (
upCmd = &cobra.Command{
Use: "up",
Short: "install, login and start wiretrustee client",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
var upCmd = &cobra.Command{
Use: "up",
Short: "install, login and start wiretrustee client",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()
ctx := internal.CtxInitState(cmd.Context())
err := loginCmd.RunE(cmd, args)
// workaround to run without service
if logFile == "console" {
config, err := internal.GetConfig(managementURL, configPath, preSharedKey)
if err != nil {
log.Errorf("get config file: %v", err)
return err
}
if logFile == "console" {
SetupCloseHandler()
return runClient()
}
s, err := newSVC(&program{}, newSVCConfig())
err = WithBackOff(func() error {
return internal.Login(ctx, config, setupKey)
})
if err != nil {
cmd.PrintErrln(err)
log.Errorf("backoff cycle failed: %v", err)
return err
}
srvStatus, err := s.Status()
if err != nil {
if err == service.ErrNotInstalled {
log.Infof("%s. Installing it now", err.Error())
e := installCmd.RunE(cmd, args)
if e != nil {
return e
}
} else {
log.Warnf("failed retrieving service status: %v", err)
}
}
if srvStatus == service.StatusRunning {
stopCmd.Run(cmd, args)
}
return startCmd.RunE(cmd, args)
},
}
)
// createEngineConfig converts configuration received from Management Service to EngineConfig
func createEngineConfig(key wgtypes.Key, config *internal.Config, peerConfig *mgmProto.PeerConfig) (*internal.EngineConfig, error) {
iFaceBlackList := make(map[string]struct{})
for i := 0; i < len(config.IFaceBlackList); i += 2 {
iFaceBlackList[config.IFaceBlackList[i]] = struct{}{}
}
engineConf := &internal.EngineConfig{
WgIfaceName: config.WgIface,
WgAddr: peerConfig.Address,
IFaceBlackList: iFaceBlackList,
WgPrivateKey: key,
WgPort: internal.WgPort,
}
if config.PreSharedKey != "" {
preSharedKey, err := wgtypes.ParseKey(config.PreSharedKey)
if err != nil {
return nil, err
SetupCloseHandler()
return internal.RunClient(ctx, config, stopCh, cleanupCh)
}
engineConf.PreSharedKey = &preSharedKey
}
return engineConf, nil
}
// connectToSignal creates Signal Service client and established a connection
func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig, ourPrivateKey wgtypes.Key) (*signal.GrpcClient, error) {
var sigTLSEnabled bool
if wtConfig.Signal.Protocol == mgmProto.HostConfig_HTTPS {
sigTLSEnabled = true
} else {
sigTLSEnabled = false
}
signalClient, err := signal.NewClient(ctx, wtConfig.Signal.Uri, ourPrivateKey, sigTLSEnabled)
if err != nil {
log.Errorf("error while connecting to the Signal Exchange Service %s: %s", wtConfig.Signal.Uri, err)
return nil, status.Errorf(codes.FailedPrecondition, "failed connecting to Signal Service : %s", err)
}
return signalClient, nil
}
// connectToManagement creates Management Services client, establishes a connection, logs-in and gets a global Wiretrustee config (signal, turn, stun hosts, etc)
func connectToManagement(ctx context.Context, managementAddr string, ourPrivateKey wgtypes.Key, tlsEnabled bool) (*mgm.GrpcClient, *mgmProto.LoginResponse, error) {
log.Debugf("connecting to management server %s", managementAddr)
client, err := mgm.NewClient(ctx, managementAddr, ourPrivateKey, tlsEnabled)
if err != nil {
return nil, nil, status.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err)
}
log.Debugf("connected to management server %s", managementAddr)
serverPublicKey, err := client.GetServerPublicKey()
if err != nil {
return nil, nil, status.Errorf(codes.FailedPrecondition, "failed while getting Management Service public key: %s", err)
}
loginResp, err := client.Login(*serverPublicKey)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
log.Error("peer registration required. Please run wiretrustee login command first")
return nil, nil, err
} else {
return nil, nil, err
}
}
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
}
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
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
log.Errorf("failed to connect to service CLI interface %v", err)
return err
}
defer conn.Close()
daemonClient := proto.NewDaemonServiceClient(conn)
loginRequest := proto.LoginRequest{
SetupKey: setupKey,
PresharedKey: preSharedKey,
ManagementUrl: managementURL,
}
err = WithBackOff(func() error {
_, err := daemonClient.Login(ctx, &loginRequest)
return err
})
if err != nil {
log.Errorf("backoff cycle failed: %v", err)
return err
}
status, err := daemonClient.Status(ctx, &proto.StatusRequest{})
if err != nil {
log.Errorf("get status: %v", err)
return err
}
if status.Status != string(internal.StatusIdle) {
log.Warnf("already connected")
return nil
}
if _, err := daemonClient.Up(ctx, &proto.UpRequest{}); err != nil {
log.Errorf("call service up method: %v", err)
return err
}
return nil
},
}

View File

@@ -0,0 +1,98 @@
package cmd
import (
"context"
"path/filepath"
"testing"
"time"
"github.com/wiretrustee/wiretrustee/client/internal"
mgmt "github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/util"
)
func TestUpDaemon_Start(t *testing.T) {
config := &mgmt.Config{}
_, err := util.ReadJson("../testdata/management.json", config)
if err != nil {
t.Fatal(err)
}
testDir := t.TempDir()
config.Datadir = testDir
err = util.CopyFileContents("../testdata/store.json", filepath.Join(testDir, "store.json"))
if err != nil {
t.Fatal(err)
}
_, signalLis := startSignal(t)
signalAddr = signalLis.Addr().String()
config.Signal.URI = signalAddr
_, mgmLis := startManagement(t, config)
mgmAddr = mgmLis.Addr().String()
}
func TestUpDaemon(t *testing.T) {
tempDir := t.TempDir()
confPath := tempDir + "/config.json"
stopCh = make(chan int, 1)
cleanupCh = make(chan struct{}, 1)
ctx := internal.CtxInitState(context.Background())
state := internal.CtxGetState(ctx)
_, cliLis := startClientDaemon(t, ctx, "http://"+mgmAddr, confPath, stopCh, cleanupCh)
cliAddr = cliLis.Addr().String()
daemonAddr = "tcp://" + cliAddr
rootCmd.SetArgs([]string{
"login",
"--daemon-addr", "tcp://" + cliAddr,
"--setup-key", "A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
"--log-file", "",
})
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
return
}
time.Sleep(time.Second * 3)
if status, err := state.Status(); err != nil && status != internal.StatusIdle {
t.Errorf("wrong status after login: %s, %v", internal.StatusIdle, err)
return
}
rootCmd.SetArgs([]string{
"up",
"--daemon-addr", "tcp://" + cliAddr,
"--log-file", "",
})
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
return
}
time.Sleep(time.Second * 3)
if status, err := state.Status(); err != nil && status != internal.StatusConnected {
t.Errorf("wrong status after connect: %s, %v", status, err)
return
}
rootCmd.SetArgs([]string{
"status",
"--daemon-addr", "tcp://" + cliAddr,
})
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
return
}
time.Sleep(time.Second * 3)
rootCmd.SetErr(nil)
rootCmd.SetArgs([]string{"down", "--daemon-addr", "tcp://" + cliAddr})
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
return
}
// we can't check status here, because context already canceled
}

View File

@@ -1,16 +1,20 @@
package cmd
import (
"github.com/wiretrustee/wiretrustee/iface"
mgmt "github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/util"
"net/url"
"path/filepath"
"testing"
"time"
"github.com/wiretrustee/wiretrustee/iface"
mgmt "github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/util"
)
var signalAddr string
var (
signalAddr string
cliAddr string
)
func TestUp_Start(t *testing.T) {
config := &mgmt.Config{}
@@ -29,15 +33,11 @@ func TestUp_Start(t *testing.T) {
signalAddr = signalLis.Addr().String()
config.Signal.URI = signalAddr
_, mgmLis := startManagement(config, t)
_, mgmLis := startManagement(t, config)
mgmAddr = mgmLis.Addr().String()
}
func TestUp(t *testing.T) {
//defer iface.Close("wt0")
tempDir := t.TempDir()
confPath := tempDir + "/config.json"
mgmtURL, err := url.Parse("http://" + mgmAddr)
@@ -58,12 +58,13 @@ func TestUp(t *testing.T) {
"--log-file",
"console",
})
go func() {
err = rootCmd.Execute()
if err != nil {
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
}
}()
time.Sleep(time.Second * 2)
timeout := 15 * time.Second
timeoutChannel := time.After(timeout)

210
client/internal/connect.go Normal file
View File

@@ -0,0 +1,210 @@
package internal
import (
"context"
"time"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/iface"
mgm "github.com/wiretrustee/wiretrustee/management/client"
mgmProto "github.com/wiretrustee/wiretrustee/management/proto"
signal "github.com/wiretrustee/wiretrustee/signal/client"
"github.com/cenkalti/backoff/v4"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// RunClient with main logic.
func RunClient(
ctx context.Context, config *Config, stopCh <-chan int, cleanupCh chan<- struct{},
) error {
backOff := &backoff.ExponentialBackOff{
InitialInterval: time.Second,
RandomizationFactor: backoff.DefaultRandomizationFactor,
Multiplier: backoff.DefaultMultiplier,
MaxInterval: 10 * time.Second,
MaxElapsedTime: 24 * 3 * time.Hour, // stop the client after 3 days trying (must be a huge problem, e.g permission denied)
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}
state := CtxGetState(ctx)
defer state.Set(StatusIdle)
wrapErr := state.Wrap
operation := func() error {
// if context cancelled we not start new backoff cycle
select {
case <-ctx.Done():
return nil
default:
}
state.Set(StatusConnecting)
// validate our peer's Wireguard PRIVATE key
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
if err != nil {
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
return wrapErr(err)
}
var mgmTlsEnabled bool
if config.ManagementURL.Scheme == "https" {
mgmTlsEnabled = true
}
// 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 wrapErr(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 wrapErr(err)
}
peerConfig := loginResp.GetPeerConfig()
engineConfig, err := createEngineConfig(myPrivateKey, config, peerConfig)
if err != nil {
log.Error(err)
return wrapErr(err)
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
engine := NewEngine(ctx, cancel, signalClient, mgmClient, engineConfig)
err = engine.Start()
if err != nil {
log.Errorf("error while starting Wiretrustee Connection Engine: %s", err)
return wrapErr(err)
}
log.Print("Wiretrustee engine started, my IP is: ", peerConfig.Address)
state.Set(StatusConnected)
select {
case <-stopCh:
case <-ctx.Done():
}
backOff.Reset()
err = mgmClient.Close()
if err != nil {
log.Errorf("failed closing Management Service client %v", err)
return wrapErr(err)
}
err = signalClient.Close()
if err != nil {
log.Errorf("failed closing Signal Service client %v", err)
return wrapErr(err)
}
err = engine.Stop()
if err != nil {
log.Errorf("failed stopping engine %v", err)
return wrapErr(err)
}
go func() {
cleanupCh <- struct{}{}
}()
log.Info("stopped Wiretrustee client")
if _, err := state.Status(); err == ErrResetConnection {
return err
}
return nil
}
err := backoff.Retry(operation, backOff)
if err != nil {
log.Errorf("exiting client retry loop due to unrecoverable error: %s", err)
return err
}
return nil
}
// createEngineConfig converts configuration received from Management Service to EngineConfig
func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) {
iFaceBlackList := make(map[string]struct{})
for i := 0; i < len(config.IFaceBlackList); i += 2 {
iFaceBlackList[config.IFaceBlackList[i]] = struct{}{}
}
engineConf := &EngineConfig{
WgIfaceName: config.WgIface,
WgAddr: peerConfig.Address,
IFaceBlackList: iFaceBlackList,
WgPrivateKey: key,
WgPort: iface.DefaultWgPort,
}
if config.PreSharedKey != "" {
preSharedKey, err := wgtypes.ParseKey(config.PreSharedKey)
if err != nil {
return nil, err
}
engineConf.PreSharedKey = &preSharedKey
}
return engineConf, nil
}
// connectToSignal creates Signal Service client and established a connection
func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig, ourPrivateKey wgtypes.Key) (*signal.GrpcClient, error) {
var sigTLSEnabled bool
if wtConfig.Signal.Protocol == mgmProto.HostConfig_HTTPS {
sigTLSEnabled = true
} else {
sigTLSEnabled = false
}
signalClient, err := signal.NewClient(ctx, wtConfig.Signal.Uri, ourPrivateKey, sigTLSEnabled)
if err != nil {
log.Errorf("error while connecting to the Signal Exchange Service %s: %s", wtConfig.Signal.Uri, err)
return nil, status.Errorf(codes.FailedPrecondition, "failed connecting to Signal Service : %s", err)
}
return signalClient, nil
}
// connectToManagement creates Management Services client, establishes a connection, logs-in and gets a global Wiretrustee config (signal, turn, stun hosts, etc)
func connectToManagement(ctx context.Context, managementAddr string, ourPrivateKey wgtypes.Key, tlsEnabled bool) (*mgm.GrpcClient, *mgmProto.LoginResponse, error) {
log.Debugf("connecting to management server %s", managementAddr)
client, err := mgm.NewClient(ctx, managementAddr, ourPrivateKey, tlsEnabled)
if err != nil {
return nil, nil, status.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err)
}
log.Debugf("connected to management server %s", managementAddr)
serverPublicKey, err := client.GetServerPublicKey()
if err != nil {
return nil, nil, status.Errorf(codes.FailedPrecondition, "failed while getting Management Service public key: %s", err)
}
loginResp, err := client.Login(*serverPublicKey)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
log.Error("peer registration required. Please run wiretrustee login command first")
return nil, nil, err
} else {
return nil, nil, err
}
}
log.Debugf("peer logged in to Management Service %s", managementAddr)
return client, loginResp, nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math/rand"
"net"
"strings"
"sync"
"time"
@@ -22,25 +23,37 @@ import (
)
// PeerConnectionTimeoutMax is a timeout of an initial connection attempt to a remote peer.
// E.g. this peer will wait PeerConnectionTimeoutMax for the remote peer to respond, if not successful then it will retry the connection attempt.
// E.g. this peer will wait PeerConnectionTimeoutMax for the remote peer to respond,
// if not successful then it will retry the connection attempt.
// Todo pass timeout at EnginConfig
const PeerConnectionTimeoutMax = 45000 //ms
const PeerConnectionTimeoutMin = 30000 //ms
const (
PeerConnectionTimeoutMax = 45000 // ms
PeerConnectionTimeoutMin = 30000 // ms
)
const WgPort = 51820
var ErrResetConnection = fmt.Errorf("reset connection")
// EngineConfig is a config for the Engine
type EngineConfig struct {
WgPort int
WgIfaceName string
// WgAddr is a Wireguard local address (Wiretrustee Network IP)
WgAddr string
// WgPrivateKey is a Wireguard private key of our peer (it MUST never leave the machine)
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
// UDPMuxPort default value 0 - the system will pick an available port
UDPMuxPort int
// UDPMuxSrflxPort default value 0 - the system will pick an available port
UDPMuxSrflxPort int
}
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
@@ -67,6 +80,11 @@ type Engine struct {
wgInterface iface.WGIface
udpMux ice.UDPMux
udpMuxSrflx ice.UniversalUDPMux
udpMuxConn *net.UDPConn
udpMuxConnSrflx *net.UDPConn
// networkSerial is the latest Serial (state ID) of the network sent by the Management service
networkSerial uint64
}
@@ -78,8 +96,13 @@ type Peer struct {
}
// NewEngine creates a new Connection Engine
func NewEngine(signalClient signal.Client, mgmClient mgm.Client, config *EngineConfig, cancel context.CancelFunc, ctx context.Context) *Engine {
func NewEngine(
ctx context.Context, cancel context.CancelFunc,
signalClient signal.Client, mgmClient mgm.Client, config *EngineConfig,
) *Engine {
return &Engine{
ctx: ctx,
cancel: cancel,
signal: signalClient,
mgmClient: mgmClient,
peerConns: map[string]*peer.Conn{},
@@ -87,8 +110,6 @@ func NewEngine(signalClient signal.Client, mgmClient mgm.Client, config *EngineC
config: config,
STUNs: []*ice.URL{},
TURNs: []*ice.URL{},
cancel: cancel,
ctx: ctx,
networkSerial: 0,
}
}
@@ -111,6 +132,30 @@ func (e *Engine) Stop() error {
}
}
if e.udpMux != nil {
if err := e.udpMux.Close(); err != nil {
log.Debugf("close udp mux: %v", err)
}
}
if e.udpMuxSrflx != nil {
if err := e.udpMuxSrflx.Close(); err != nil {
log.Debugf("close server reflexive udp mux: %v", err)
}
}
if e.udpMuxConn != nil {
if err := e.udpMuxConn.Close(); err != nil {
log.Debugf("close udp mux connection: %v", err)
}
}
if e.udpMuxConnSrflx != nil {
if err := e.udpMuxConnSrflx.Close(); err != nil {
log.Debugf("close server reflexive udp mux connection: %v", err)
}
}
log.Infof("stopped Wiretrustee Engine")
return nil
@@ -134,6 +179,21 @@ func (e *Engine) Start() error {
return err
}
e.udpMuxConn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: e.config.UDPMuxPort})
if err != nil {
log.Errorf("failed listening on UDP port %d: [%s]", e.config.UDPMuxPort, err.Error())
return err
}
e.udpMuxConnSrflx, err = net.ListenUDP("udp4", &net.UDPAddr{Port: e.config.UDPMuxSrflxPort})
if err != nil {
log.Errorf("failed listening on UDP port %d: [%s]", e.config.UDPMuxSrflxPort, err.Error())
return err
}
e.udpMux = ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: e.udpMuxConn})
e.udpMuxSrflx = ice.NewUniversalUDPMuxDefault(ice.UniversalUDPMuxParams{UDPConn: e.udpMuxConnSrflx})
err = e.wgInterface.Create()
if err != nil {
log.Errorf("failed creating tunnel interface %s: [%s]", wgIfaceName, err.Error())
@@ -154,7 +214,6 @@ func (e *Engine) Start() error {
// removePeers finds and removes peers that do not exist anymore in the network map received from the Management Service
func (e *Engine) removePeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
currentPeers := make([]string, 0, len(e.peerConns))
for p := range e.peerConns {
currentPeers = append(currentPeers, p)
@@ -209,7 +268,6 @@ func (e *Engine) removePeer(peerKey string) error {
// GetPeerConnectionStatus returns a connection Status or nil if peer connection wasn't found
func (e *Engine) GetPeerConnectionStatus(peerKey string) peer.ConnStatus {
conn, exists := e.peerConns[peerKey]
if exists && conn != nil {
return conn.Status()
@@ -217,6 +275,7 @@ func (e *Engine) GetPeerConnectionStatus(peerKey string) peer.ConnStatus {
return -1
}
func (e *Engine) GetPeers() []string {
e.syncMsgMux.Lock()
defer e.syncMsgMux.Unlock()
@@ -254,7 +313,7 @@ func signalCandidate(candidate ice.Candidate, myKey wgtypes.Key, remoteKey wgtyp
})
if err != nil {
log.Errorf("failed signaling candidate to the remote peer %s %s", remoteKey.String(), err)
//todo ??
// todo ??
return err
}
@@ -262,7 +321,6 @@ func signalCandidate(candidate ice.Candidate, myKey wgtypes.Key, remoteKey wgtyp
}
func signalAuth(uFrag string, pwd string, myKey wgtypes.Key, remoteKey wgtypes.Key, s signal.Client, isAnswer bool) error {
var t sProto.Body_Type
if isAnswer {
t = sProto.Body_ANSWER
@@ -272,7 +330,8 @@ func signalAuth(uFrag string, pwd string, myKey wgtypes.Key, remoteKey wgtypes.K
msg, err := signal.MarshalCredential(myKey, remoteKey, &signal.Credential{
UFrag: uFrag,
Pwd: pwd}, t)
Pwd: pwd,
}, t)
if err != nil {
return err
}
@@ -299,7 +358,7 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
return err
}
//todo update signal
// todo update signal
}
if update.GetNetworkMap() != nil {
@@ -311,7 +370,6 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
}
return nil
}
// receiveManagementEvents connects to the Management Service event stream to receive updates from the management service
@@ -324,6 +382,7 @@ func (e *Engine) receiveManagementEvents() {
if err != nil {
// happens if management is unavailable for a long time.
// We want to cancel the operation of the whole client
_ = CtxGetState(e.ctx).Wrap(ErrResetConnection)
e.cancel()
return
}
@@ -371,7 +430,6 @@ func (e *Engine) updateTURNs(turns []*mgmProto.ProtectedHostConfig) error {
}
func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
serial := networkMap.GetSerial()
if e.networkSerial > serial {
log.Debugf("received outdated NetworkMap with serial %d, ignoring", serial)
@@ -455,7 +513,6 @@ func (e Engine) peerExists(peerKey string) bool {
}
func (e Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, error) {
var stunTurn []*ice.URL
stunTurn = append(stunTurn, e.STUNs...)
stunTurn = append(stunTurn, e.TURNs...)
@@ -481,6 +538,8 @@ func (e Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, er
StunTurn: stunTurn,
InterfaceBlackList: interfaceBlacklist,
Timeout: timeout,
UDPMux: e.udpMux,
UDPMuxSrflx: e.udpMuxSrflx,
ProxyConfig: proxyConfig,
}
@@ -515,11 +574,9 @@ func (e Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, er
// receiveSignalEvents connects to the Signal Service event stream to negotiate connection with remote peers
func (e *Engine) receiveSignalEvents() {
go func() {
// connect to a stream of messages coming from the signal server
err := e.signal.Receive(func(msg *sProto.Message) error {
e.syncMsgMux.Lock()
defer e.syncMsgMux.Unlock()
@@ -561,6 +618,7 @@ func (e *Engine) receiveSignalEvents() {
if err != nil {
// happens if signal is unavailable for a long time.
// We want to cancel the operation of the whole client
_ = CtxGetState(e.ctx).Wrap(ErrResetConnection)
e.cancel()
return
}

View File

@@ -40,7 +40,6 @@ var (
)
func TestEngine_UpdateNetworkMap(t *testing.T) {
// test setup
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
@@ -51,12 +50,12 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
engine := NewEngine(&signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
WgIfaceName: "utun100",
WgAddr: "100.64.0.1/24",
WgPrivateKey: key,
WgPort: 33100,
}, cancel, ctx)
})
type testCase struct {
name string
@@ -157,7 +156,6 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
}
for _, c := range []testCase{case1, case2, case3, case4, case5} {
t.Run(c.name, func(t *testing.T) {
err = engine.updateNetworkMap(c.networkMap)
if err != nil {
@@ -179,13 +177,10 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
}
}
})
}
}
func TestEngine_Sync(t *testing.T) {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fatal(err)
@@ -199,7 +194,6 @@ func TestEngine_Sync(t *testing.T) {
updates := make(chan *mgmtProto.SyncResponse)
defer close(updates)
syncFunc := func(msgHandler func(msg *mgmtProto.SyncResponse) error) error {
for msg := range updates {
err := msgHandler(msg)
if err != nil {
@@ -209,12 +203,12 @@ func TestEngine_Sync(t *testing.T) {
return nil
}
engine := NewEngine(&signal.MockClient{}, &mgmt.MockClient{SyncFunc: syncFunc}, &EngineConfig{
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{SyncFunc: syncFunc}, &EngineConfig{
WgIfaceName: "utun100",
WgAddr: "100.64.0.1/24",
WgPrivateKey: key,
WgPort: 33100,
}, cancel, ctx)
})
defer func() {
err := engine.Stop()
@@ -256,6 +250,7 @@ func TestEngine_Sync(t *testing.T) {
select {
case <-timeout:
t.Fatalf("timeout while waiting for test to finish")
return
default:
}
@@ -263,12 +258,10 @@ func TestEngine_Sync(t *testing.T) {
break
}
}
}
func TestEngine_MultiplePeers(t *testing.T) {
//log.SetLevel(log.DebugLevel)
// log.SetLevel(log.DebugLevel)
dir := t.TempDir()
@@ -284,8 +277,9 @@ func TestEngine_MultiplePeers(t *testing.T) {
}
}()
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
defer cancel()
sport := 10010
sigServer, err := startSignal(sport)
if err != nil {
@@ -365,7 +359,6 @@ func TestEngine_MultiplePeers(t *testing.T) {
}
func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey string, i int, mport int, sport int) (*Engine, error) {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
@@ -397,14 +390,15 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin
ifaceName = fmt.Sprintf("wt%d", i)
}
wgPort := 33100 + i
conf := &EngineConfig{
WgIfaceName: ifaceName,
WgAddr: resp.PeerConfig.Address,
WgPrivateKey: key,
WgPort: 33100 + i,
WgPort: wgPort,
}
return NewEngine(signalClient, mgmtClient, conf, cancel, ctx), nil
return NewEngine(ctx, cancel, signalClient, mgmtClient, conf), nil
}
func startSignal(port int) (*grpc.Server, error) {
@@ -427,7 +421,6 @@ func startSignal(port int) (*grpc.Server, error) {
}
func startManagement(port int, dataDir string) (*grpc.Server, error) {
config := &server.Config{
Stuns: []*server.Host{},
TURNConfig: &server.TURNConfig{},

118
client/internal/login.go Normal file
View File

@@ -0,0 +1,118 @@
package internal
import (
"context"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/client/system"
mgm "github.com/wiretrustee/wiretrustee/management/client"
mgmProto "github.com/wiretrustee/wiretrustee/management/proto"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func Login(ctx context.Context, config *Config, setupKey string) error {
backOff := &backoff.ExponentialBackOff{
InitialInterval: time.Second,
RandomizationFactor: backoff.DefaultRandomizationFactor,
Multiplier: backoff.DefaultMultiplier,
MaxInterval: 2 * time.Second,
MaxElapsedTime: time.Second * 10,
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}
// 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
}
var mgmTlsEnabled bool
if config.ManagementURL.Scheme == "https" {
mgmTlsEnabled = true
}
loginOp := func() error {
log.Debugf("connecting to Management Service %s", config.ManagementURL.String())
mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
if err != nil {
log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), err)
return err
}
log.Debugf("connected to management Service %s", config.ManagementURL.String())
serverKey, err := mgmClient.GetServerPublicKey()
if err != nil {
log.Errorf("failed while getting Management Service public key: %v", err)
return err
}
_, err = loginPeer(*serverKey, mgmClient, setupKey)
if err != nil {
log.Errorf("failed logging-in peer on Management Service : %v", err)
return err
}
err = mgmClient.Close()
if err != nil {
log.Errorf("failed closing Management Service client: %v", err)
return err
}
return nil
}
err = backoff.RetryNotify(loginOp, backOff, func(err error, duration time.Duration) {
log.Warnf("retrying Login to the Management service in %v due to error %v", duration, err)
})
if err != nil {
log.Errorf("exiting login retry loop due to unrecoverable error: %v", err)
return err
}
return nil
}
// loginPeer attempts to login to Management Service. If peer wasn't registered, tries the registration flow.
func loginPeer(serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string) (*mgmProto.LoginResponse, error) {
loginResp, err := client.Login(serverPublicKey)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
log.Debugf("peer registration required")
return registerPeer(serverPublicKey, client, setupKey)
} else {
return nil, err
}
}
log.Info("peer has successfully logged-in to Management Service")
return loginResp, nil
}
// registerPeer checks whether setupKey was provided via cmd line and if not then it prompts user to enter a key.
// Otherwise tries to register with the provided setupKey via command line.
func registerPeer(serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string) (*mgmProto.LoginResponse, error) {
validSetupKey, err := uuid.Parse(setupKey)
if err != nil {
return nil, err
}
log.Debugf("sending peer registration request to Management Service")
info := system.GetInfo()
loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), info)
if err != nil {
log.Errorf("failed registering peer %v", err)
return nil, err
}
log.Infof("peer has been successfully registered on Management Service")
return loginResp, nil
}

View File

@@ -2,12 +2,15 @@ package peer
import (
"context"
"github.com/pion/ice/v2"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/client/internal/proxy"
"github.com/wiretrustee/wiretrustee/iface"
"golang.zx2c4.com/wireguard/wgctrl"
"net"
"sync"
"time"
"github.com/pion/ice/v2"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/client/internal/proxy"
)
// ConnConfig is a peer Connection configuration
@@ -28,6 +31,9 @@ type ConnConfig struct {
Timeout time.Duration
ProxyConfig proxy.Config
UDPMux ice.UDPMux
UDPMuxSrflx ice.UniversalUDPMux
}
// IceCredentials ICE protocol credentials struct
@@ -83,11 +89,20 @@ func interfaceFilter(blackList []string) func(string) bool {
}
}
return func(iFace string) bool {
if len(blackListMap) == 0 {
return true
}
_, ok := blackListMap[iFace]
return !ok
if ok {
return false
}
// look for unlisted Wireguard interfaces
wg, err := wgctrl.New()
if err != nil {
log.Debugf("trying to create a wgctrl client failed with: %v", err)
}
defer wg.Close()
_, err = wg.Device(iFace)
return err != nil
}
}
@@ -104,6 +119,8 @@ func (conn *Conn) reCreateAgent() error {
CandidateTypes: []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive, ice.CandidateTypeRelay},
FailedTimeout: &failedTimeout,
InterfaceFilter: interfaceFilter(conn.config.InterfaceBlackList),
UDPMux: conn.config.UDPMux,
UDPMuxSrflx: conn.config.UDPMuxSrflx,
})
if err != nil {
return err
@@ -174,7 +191,7 @@ func (conn *Conn) Open() error {
log.Debugf("received connection confirmation from peer %s", conn.config.Key)
//at this point we received offer/answer and we are ready to gather candidates
// at this point we received offer/answer and we are ready to gather candidates
conn.mu.Lock()
conn.status = StatusConnecting
conn.ctx, conn.notifyDisconnected = context.WithCancel(context.Background())
@@ -206,7 +223,14 @@ func (conn *Conn) Open() error {
return err
}
log.Infof("connected to peer %s [laddr <-> raddr] [%s <-> %s]", conn.config.Key, remoteConn.LocalAddr().String(), remoteConn.RemoteAddr().String())
if conn.proxy.Type() == proxy.TypeNoProxy {
host, _, _ := net.SplitHostPort(remoteConn.LocalAddr().String())
rhost, _, _ := net.SplitHostPort(remoteConn.RemoteAddr().String())
// direct Wireguard connection
log.Infof("directly connected to peer %s [laddr <-> raddr] [%s:%d <-> %s:%d]", conn.config.Key, host, iface.DefaultWgPort, rhost, iface.DefaultWgPort)
} else {
log.Infof("connected to peer %s [laddr <-> raddr] [%s <-> %s]", conn.config.Key, remoteConn.LocalAddr().String(), remoteConn.RemoteAddr().String())
}
// wait until connection disconnected or has been closed externally (upper layer, e.g. engine)
select {
@@ -219,16 +243,65 @@ func (conn *Conn) Open() error {
}
}
// useProxy determines whether a direct connection (without a go proxy) is possible
// There are 3 cases: one of the peers has a public IP or both peers are in the same private network
// Please note, that this check happens when peers were already able to ping each other using ICE layer.
func shouldUseProxy(pair *ice.CandidatePair) bool {
remoteIP := net.ParseIP(pair.Remote.Address())
myIp := net.ParseIP(pair.Local.Address())
remoteIsPublic := IsPublicIP(remoteIP)
myIsPublic := IsPublicIP(myIp)
//one of the hosts has a public IP
if remoteIsPublic && pair.Remote.Type() == ice.CandidateTypeHost {
return false
}
if myIsPublic && pair.Local.Type() == ice.CandidateTypeHost {
return false
}
if pair.Local.Type() == ice.CandidateTypeHost && pair.Remote.Type() == ice.CandidateTypeHost {
if !remoteIsPublic && !myIsPublic {
//both hosts are in the same private network
return false
}
}
return true
}
// IsPublicIP indicates whether IP is public or not.
func IsPublicIP(ip net.IP) bool {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsPrivate() {
return false
}
return true
}
// startProxy starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
func (conn *Conn) startProxy(remoteConn net.Conn) error {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.proxy = proxy.NewWireguardProxy(conn.config.ProxyConfig)
err := conn.proxy.Start(remoteConn)
var pair *ice.CandidatePair
pair, err := conn.agent.GetSelectedCandidatePair()
if err != nil {
return err
}
useProxy := shouldUseProxy(pair)
var p proxy.Proxy
if useProxy {
p = proxy.NewWireguardProxy(conn.config.ProxyConfig)
} else {
p = proxy.NewNoProxy(conn.config.ProxyConfig)
}
conn.proxy = p
err = p.Start(remoteConn)
if err != nil {
return err
}
conn.status = StatusConnected
return nil
@@ -287,7 +360,7 @@ func (conn *Conn) SetSignalCandidate(handler func(candidate ice.Candidate) error
// and then signals them to the remote peer
func (conn *Conn) onICECandidate(candidate ice.Candidate) {
if candidate != nil {
//log.Debugf("discovered local candidate %s", candidate.String())
// log.Debugf("discovered local candidate %s", candidate.String())
go func() {
err := conn.signalCandidate(candidate)
if err != nil {
@@ -298,8 +371,8 @@ func (conn *Conn) onICECandidate(candidate ice.Candidate) {
}
func (conn *Conn) onICESelectedCandidatePair(c1 ice.Candidate, c2 ice.Candidate) {
log.Debugf("selected candidate pair [local <-> remote] -> [%s <-> %s], peer %s", conn.config.Key,
c1.String(), c2.String())
log.Debugf("selected candidate pair [local <-> remote] -> [%s <-> %s], peer %s", c1.String(), c2.String(),
conn.config.Key)
}
// onICEConnectionStateChange registers callback of an ICE Agent to track connection state
@@ -386,7 +459,7 @@ func (conn *Conn) OnRemoteOffer(remoteAuth IceCredentials) bool {
return true
default:
log.Debugf("OnRemoteOffer skipping message from peer %s on status %s because is not ready", conn.config.Key, conn.status.String())
//connection might not be ready yet to receive so we ignore the message
// connection might not be ready yet to receive so we ignore the message
return false
}
}
@@ -400,7 +473,7 @@ func (conn *Conn) OnRemoteAnswer(remoteAuth IceCredentials) bool {
case conn.remoteAnswerCh <- remoteAuth:
return true
default:
//connection might not be ready yet to receive so we ignore the message
// connection might not be ready yet to receive so we ignore the message
log.Debugf("OnRemoteAnswer skipping message from peer %s on status %s because is not ready", conn.config.Key, conn.status.String())
return false
}

View File

@@ -66,3 +66,7 @@ func (p *DummyProxy) Start(remoteConn net.Conn) error {
return nil
}
func (p *DummyProxy) Type() Type {
return TypeDummy
}

View File

@@ -0,0 +1,52 @@
package proxy
import (
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/iface"
"net"
)
// NoProxy is used when there is no need for a proxy between ICE and Wireguard.
// This is possible in either of these cases:
// - peers are in the same local network
// - one of the peers has a public static IP (host)
// NoProxy will just update remote peer with a remote host and fixed Wireguard port (r.g. 51820).
// In order NoProxy to work, Wireguard port has to be fixed for the time being.
type NoProxy struct {
config Config
}
func NewNoProxy(config Config) *NoProxy {
return &NoProxy{config: config}
}
func (p *NoProxy) Close() error {
err := p.config.WgInterface.RemovePeer(p.config.RemoteKey)
if err != nil {
return err
}
return nil
}
// Start just updates Wireguard peer with the remote IP and default Wireguard port
func (p *NoProxy) Start(remoteConn net.Conn) error {
log.Debugf("using NoProxy while connecting to peer %s", p.config.RemoteKey)
addr, err := net.ResolveUDPAddr("udp", remoteConn.RemoteAddr().String())
if err != nil {
return err
}
addr.Port = iface.DefaultWgPort
err = p.config.WgInterface.UpdatePeer(p.config.RemoteKey, p.config.AllowedIps, DefaultWgKeepAlive,
addr, p.config.PreSharedKey)
if err != nil {
return err
}
return nil
}
func (p *NoProxy) Type() Type {
return TypeNoProxy
}

View File

@@ -10,6 +10,14 @@ import (
const DefaultWgKeepAlive = 25 * time.Second
type Type string
const (
TypeNoProxy Type = "NoProxy"
TypeWireguard Type = "Wireguard"
TypeDummy Type = "Dummy"
)
type Config struct {
WgListenAddr string
RemoteKey string
@@ -22,4 +30,5 @@ type Proxy interface {
io.Closer
// Start creates a local remoteConn and starts proxying data from/to remoteConn
Start(remoteConn net.Conn) error
Type() Type
}

View File

@@ -122,3 +122,7 @@ func (p *WireguardProxy) proxyToLocal() {
}
}
}
func (p *WireguardProxy) Type() Type {
return TypeWireguard
}

67
client/internal/state.go Normal file
View File

@@ -0,0 +1,67 @@
package internal
import (
"context"
"sync"
)
type StatusType string
const (
StatusIdle StatusType = "Idle"
StatusConnecting StatusType = "Connecting"
StatusConnected StatusType = "Connected"
)
// CtxInitState setup context state into the context tree.
//
// This function should be used to initialize context before
// CtxGetState will be executed.
func CtxInitState(ctx context.Context) context.Context {
return context.WithValue(ctx, stateCtx, &contextState{
status: StatusIdle,
})
}
// CtxGetState object to get/update state/errors of process.
func CtxGetState(ctx context.Context) *contextState {
return ctx.Value(stateCtx).(*contextState)
}
type contextState struct {
err error
status StatusType
mutex sync.Mutex
}
func (c *contextState) Set(update StatusType) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.status = update
c.err = nil
}
func (c *contextState) Status() (StatusType, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.err != nil {
return "", c.err
}
return c.status, nil
}
func (c *contextState) Wrap(err error) error {
c.mutex.Lock()
defer c.mutex.Unlock()
c.err = err
return err
}
type stateKey int
var stateCtx stateKey

566
client/proto/daemon.pb.go Normal file
View File

@@ -0,0 +1,566 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.17.3
// source: daemon.proto
package proto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
_ "google.golang.org/protobuf/types/descriptorpb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type LoginRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// setupKey wiretrustee setup key.
SetupKey string `protobuf:"bytes,1,opt,name=setupKey,proto3" json:"setupKey,omitempty"`
// presharedKey for wireguard setup.
PresharedKey string `protobuf:"bytes,2,opt,name=presharedKey,proto3" json:"presharedKey,omitempty"`
// managementUrl to authenticate.
ManagementUrl string `protobuf:"bytes,3,opt,name=managementUrl,proto3" json:"managementUrl,omitempty"`
}
func (x *LoginRequest) Reset() {
*x = LoginRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_daemon_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *LoginRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoginRequest) ProtoMessage() {}
func (x *LoginRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead.
func (*LoginRequest) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{0}
}
func (x *LoginRequest) GetSetupKey() string {
if x != nil {
return x.SetupKey
}
return ""
}
func (x *LoginRequest) GetPresharedKey() string {
if x != nil {
return x.PresharedKey
}
return ""
}
func (x *LoginRequest) GetManagementUrl() string {
if x != nil {
return x.ManagementUrl
}
return ""
}
type LoginResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *LoginResponse) Reset() {
*x = LoginResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_daemon_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *LoginResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoginResponse) ProtoMessage() {}
func (x *LoginResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead.
func (*LoginResponse) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{1}
}
type UpRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *UpRequest) Reset() {
*x = UpRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_daemon_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UpRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpRequest) ProtoMessage() {}
func (x *UpRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpRequest.ProtoReflect.Descriptor instead.
func (*UpRequest) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{2}
}
type UpResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *UpResponse) Reset() {
*x = UpResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_daemon_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UpResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpResponse) ProtoMessage() {}
func (x *UpResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpResponse.ProtoReflect.Descriptor instead.
func (*UpResponse) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{3}
}
type StatusRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *StatusRequest) Reset() {
*x = StatusRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_daemon_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *StatusRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StatusRequest) ProtoMessage() {}
func (x *StatusRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StatusRequest.ProtoReflect.Descriptor instead.
func (*StatusRequest) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{4}
}
type StatusResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// status of the server.
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
}
func (x *StatusResponse) Reset() {
*x = StatusResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_daemon_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *StatusResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StatusResponse) ProtoMessage() {}
func (x *StatusResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StatusResponse.ProtoReflect.Descriptor instead.
func (*StatusResponse) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{5}
}
func (x *StatusResponse) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
type DownRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *DownRequest) Reset() {
*x = DownRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_daemon_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DownRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DownRequest) ProtoMessage() {}
func (x *DownRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DownRequest.ProtoReflect.Descriptor instead.
func (*DownRequest) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{6}
}
type DownResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *DownResponse) Reset() {
*x = DownResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_daemon_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DownResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DownResponse) ProtoMessage() {}
func (x *DownResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DownResponse.ProtoReflect.Descriptor instead.
func (*DownResponse) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{7}
}
var File_daemon_proto protoreflect.FileDescriptor
var file_daemon_proto_rawDesc = []byte{
0x0a, 0x0c, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06,
0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x74, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69,
0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x75,
0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x74, 0x75,
0x70, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x65,
0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x73,
0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x22, 0x0f,
0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a,
0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x53, 0x74,
0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x28, 0x0a, 0x0e, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a,
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73,
0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x32, 0xe6, 0x01, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12,
0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c,
0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d,
0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a,
0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16,
0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e,
0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44,
0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a,
0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_daemon_proto_rawDescOnce sync.Once
file_daemon_proto_rawDescData = file_daemon_proto_rawDesc
)
func file_daemon_proto_rawDescGZIP() []byte {
file_daemon_proto_rawDescOnce.Do(func() {
file_daemon_proto_rawDescData = protoimpl.X.CompressGZIP(file_daemon_proto_rawDescData)
})
return file_daemon_proto_rawDescData
}
var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_daemon_proto_goTypes = []interface{}{
(*LoginRequest)(nil), // 0: daemon.LoginRequest
(*LoginResponse)(nil), // 1: daemon.LoginResponse
(*UpRequest)(nil), // 2: daemon.UpRequest
(*UpResponse)(nil), // 3: daemon.UpResponse
(*StatusRequest)(nil), // 4: daemon.StatusRequest
(*StatusResponse)(nil), // 5: daemon.StatusResponse
(*DownRequest)(nil), // 6: daemon.DownRequest
(*DownResponse)(nil), // 7: daemon.DownResponse
}
var file_daemon_proto_depIdxs = []int32{
0, // 0: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
2, // 1: daemon.DaemonService.Up:input_type -> daemon.UpRequest
4, // 2: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
6, // 3: daemon.DaemonService.Down:input_type -> daemon.DownRequest
1, // 4: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
3, // 5: daemon.DaemonService.Up:output_type -> daemon.UpResponse
5, // 6: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
7, // 7: daemon.DaemonService.Down:output_type -> daemon.DownResponse
4, // [4:8] is the sub-list for method output_type
0, // [0:4] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_daemon_proto_init() }
func file_daemon_proto_init() {
if File_daemon_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_daemon_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*LoginRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_daemon_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*LoginResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_daemon_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UpRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_daemon_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UpResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_daemon_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StatusRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_daemon_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StatusResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_daemon_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DownRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_daemon_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DownResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_daemon_proto_rawDesc,
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_daemon_proto_goTypes,
DependencyIndexes: file_daemon_proto_depIdxs,
MessageInfos: file_daemon_proto_msgTypes,
}.Build()
File_daemon_proto = out.File
file_daemon_proto_rawDesc = nil
file_daemon_proto_goTypes = nil
file_daemon_proto_depIdxs = nil
}

49
client/proto/daemon.proto Normal file
View File

@@ -0,0 +1,49 @@
syntax = "proto3";
import "google/protobuf/descriptor.proto";
option go_package = "/proto";
package daemon;
service DaemonService {
// Login uses setup key to prepare configuration for the daemon.
rpc Login(LoginRequest) returns (LoginResponse) {}
// Up starts engine work in the daemon.
rpc Up(UpRequest) returns (UpResponse) {}
// Status of the service.
rpc Status(StatusRequest) returns (StatusResponse) {}
// Down engine work in the daemon.
rpc Down(DownRequest) returns (DownResponse) {}
};
message LoginRequest {
// setupKey wiretrustee setup key.
string setupKey = 1;
// presharedKey for wireguard setup.
string presharedKey = 2;
// managementUrl to authenticate.
string managementUrl = 3;
}
message LoginResponse {}
message UpRequest {}
message UpResponse {}
message StatusRequest{}
message StatusResponse{
// status of the server.
string status = 1;
}
message DownRequest {}
message DownResponse {}

View File

@@ -0,0 +1,217 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package proto
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// DaemonServiceClient is the client API for DaemonService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type DaemonServiceClient interface {
// Login uses setup key to prepare configuration for the daemon.
Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error)
// Up starts engine work in the daemon.
Up(ctx context.Context, in *UpRequest, opts ...grpc.CallOption) (*UpResponse, error)
// Status of the service.
Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error)
// Down engine work in the daemon.
Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error)
}
type daemonServiceClient struct {
cc grpc.ClientConnInterface
}
func NewDaemonServiceClient(cc grpc.ClientConnInterface) DaemonServiceClient {
return &daemonServiceClient{cc}
}
func (c *daemonServiceClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) {
out := new(LoginResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/Login", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *daemonServiceClient) Up(ctx context.Context, in *UpRequest, opts ...grpc.CallOption) (*UpResponse, error) {
out := new(UpResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/Up", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *daemonServiceClient) Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) {
out := new(StatusResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/Status", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *daemonServiceClient) Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error) {
out := new(DownResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/Down", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// DaemonServiceServer is the server API for DaemonService service.
// All implementations must embed UnimplementedDaemonServiceServer
// for forward compatibility
type DaemonServiceServer interface {
// Login uses setup key to prepare configuration for the daemon.
Login(context.Context, *LoginRequest) (*LoginResponse, error)
// Up starts engine work in the daemon.
Up(context.Context, *UpRequest) (*UpResponse, error)
// Status of the service.
Status(context.Context, *StatusRequest) (*StatusResponse, error)
// Down engine work in the daemon.
Down(context.Context, *DownRequest) (*DownResponse, error)
mustEmbedUnimplementedDaemonServiceServer()
}
// UnimplementedDaemonServiceServer must be embedded to have forward compatible implementations.
type UnimplementedDaemonServiceServer struct {
}
func (UnimplementedDaemonServiceServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
}
func (UnimplementedDaemonServiceServer) Up(context.Context, *UpRequest) (*UpResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Up not implemented")
}
func (UnimplementedDaemonServiceServer) Status(context.Context, *StatusRequest) (*StatusResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Status not implemented")
}
func (UnimplementedDaemonServiceServer) Down(context.Context, *DownRequest) (*DownResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Down not implemented")
}
func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {}
// UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to DaemonServiceServer will
// result in compilation errors.
type UnsafeDaemonServiceServer interface {
mustEmbedUnimplementedDaemonServiceServer()
}
func RegisterDaemonServiceServer(s grpc.ServiceRegistrar, srv DaemonServiceServer) {
s.RegisterService(&DaemonService_ServiceDesc, srv)
}
func _DaemonService_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LoginRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).Login(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/Login",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).Login(ctx, req.(*LoginRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DaemonService_Up_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).Up(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/Up",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).Up(ctx, req.(*UpRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DaemonService_Status_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StatusRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).Status(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/Status",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).Status(ctx, req.(*StatusRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DaemonService_Down_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DownRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).Down(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/Down",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).Down(ctx, req.(*DownRequest))
}
return interceptor(ctx, in, info, handler)
}
// DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var DaemonService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "daemon.DaemonService",
HandlerType: (*DaemonServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Login",
Handler: _DaemonService_Login_Handler,
},
{
MethodName: "Up",
Handler: _DaemonService_Up_Handler,
},
{
MethodName: "Status",
Handler: _DaemonService_Status_Handler,
},
{
MethodName: "Down",
Handler: _DaemonService_Down_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "daemon.proto",
}

4
client/proto/generate.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
protoc -I proto/ proto/daemon.proto --go_out=. --go-grpc_out=.

166
client/server/server.go Normal file
View File

@@ -0,0 +1,166 @@
package server
import (
"context"
"fmt"
"sync"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/client/internal"
"github.com/wiretrustee/wiretrustee/client/proto"
)
// Server for service control.
type Server struct {
rootCtx context.Context
actCancel context.CancelFunc
managementURL string
configPath string
stopCh chan int
cleanupCh chan<- struct{}
mutex sync.Mutex
config *internal.Config
proto.UnimplementedDaemonServiceServer
}
// New server instance constructor.
func New(
ctx context.Context, managementURL, configPath string,
stopCh chan int, cleanupCh chan<- struct{},
) *Server {
return &Server{
rootCtx: ctx,
managementURL: managementURL,
configPath: configPath,
stopCh: stopCh,
cleanupCh: cleanupCh,
}
}
func (s *Server) Start() error {
state := internal.CtxGetState(s.rootCtx)
// if current state contains any error, return it
// in all other cases we can continue execution only if status is idle and up command was
// not in the progress or already successfully estabilished connection.
status, err := state.Status()
if err != nil {
return err
}
if status != internal.StatusIdle {
return nil
}
ctx, cancel := context.WithCancel(s.rootCtx)
s.actCancel = cancel
// if configuration exists, we just start connections.
config, err := internal.ReadConfig(s.managementURL, s.configPath)
if err != nil {
log.Warnf("no config file, skip connection stage: %v", err)
return nil
}
go func() {
if err := internal.RunClient(ctx, config, s.stopCh, s.cleanupCh); err != nil {
log.Errorf("init connections: %v", err)
}
}()
return nil
}
// Login uses setup key to prepare configuration for the daemon.
func (s *Server) Login(_ context.Context, msg *proto.LoginRequest) (*proto.LoginResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
managementURL := s.managementURL
if msg.ManagementUrl != "" {
managementURL = msg.ManagementUrl
}
config, err := internal.GetConfig(managementURL, s.configPath, msg.PresharedKey)
if err != nil {
return nil, err
}
s.config = config
// login operation uses backoff scheme to connect to management API
// we don't wait for result and return response immediately.
if err := internal.Login(s.rootCtx, s.config, msg.SetupKey); err != nil {
log.Errorf("failed login: %v", err)
return nil, err
}
return &proto.LoginResponse{}, nil
}
// Up starts engine work in the daemon.
func (s *Server) Up(_ context.Context, msg *proto.UpRequest) (*proto.UpResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
state := internal.CtxGetState(s.rootCtx)
// if current state contains any error, return it
// in all other cases we can continue execution only if status is idle and up command was
// not in the progress or already successfully estabilished connection.
status, err := state.Status()
if err != nil {
return nil, err
}
if status != internal.StatusIdle {
return nil, fmt.Errorf("up already in progress: current status %s", status)
}
// it should be nill here, but .
if s.actCancel != nil {
s.actCancel()
}
ctx, cancel := context.WithCancel(s.rootCtx)
s.actCancel = cancel
if s.config == nil {
return nil, fmt.Errorf("config is not defined, please call login command first")
}
go func() {
if err := internal.RunClient(ctx, s.config, s.stopCh, s.cleanupCh); err != nil {
log.Errorf("run client connection: %v", state.Wrap(err))
return
}
}()
return &proto.UpResponse{}, nil
}
// Down dengine work in the daemon.
func (s *Server) Down(ctx context.Context, msg *proto.DownRequest) (*proto.DownResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.actCancel == nil {
return nil, fmt.Errorf("service is not up")
}
s.actCancel()
return &proto.DownResponse{}, nil
}
// Status starts engine work in the daemon.
func (s *Server) Status(ctx context.Context, msg *proto.StatusRequest) (*proto.StatusResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
status, err := internal.CtxGetState(s.rootCtx).Status()
if err != nil {
return nil, err
}
return &proto.StatusResponse{Status: string(status)}, nil
}

96
client/ui/client_ui.go Normal file
View File

@@ -0,0 +1,96 @@
package main
import (
"context"
"fmt"
"io/ioutil"
"time"
"github.com/getlantern/systray"
log "github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open"
"github.com/wiretrustee/wiretrustee/client/internal"
"github.com/wiretrustee/wiretrustee/client/proto"
"github.com/wiretrustee/wiretrustee/client/ui/config"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
systray.Run(onReady, nil)
}
// TODO: implementation for SSO Logins
func onReady() {
wtIcon, err := ioutil.ReadFile("wiretrustee.ico")
if err != nil {
log.Warn(err)
}
if wtIcon != nil {
systray.SetTemplateIcon(wtIcon, wtIcon)
}
go func() {
up := systray.AddMenuItem("Up", "Up")
down := systray.AddMenuItem("Down", "Down")
mUrl := systray.AddMenuItem("Open UI", "wiretrustee website")
systray.AddSeparator()
mQuitOrig := systray.AddMenuItem("Quit", "Quit the whole app")
go func() {
<-mQuitOrig.ClickedCh
fmt.Println("Requesting quit")
systray.Quit()
fmt.Println("Finished quitting")
}()
for {
select {
case <-mUrl.ClickedCh:
open.Run("https://app.wiretrustee.com")
case <-up.ClickedCh:
upCmdExec()
case <-down.ClickedCh:
fmt.Println("Clicked down")
}
}
}()
}
func handleUp() {
// This is where
}
func upCmdExec() {
log.Println("executing up command..")
cfg := config.Config()
ctx := internal.CtxInitState(context.Background())
conn, err := grpc.DialContext(ctx, cfg.DaemonAddr(),
grpc.WithTimeout(5*time.Second),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock())
if err != nil {
log.Errorf("failed to connect to service CLI interface; %v", err)
return
}
daemonClient := proto.NewDaemonServiceClient(conn)
status, err := daemonClient.Status(ctx, &proto.StatusRequest{})
if err != nil {
log.Errorf("get status: %v", err)
return
}
if status.Status != string(internal.StatusIdle) {
log.Warnf("already connected")
return
}
_, err = daemonClient.Up(ctx, &proto.UpRequest{})
if err != nil {
log.Errorf("Failed to start up client; %v", err)
return
}
}

View File

@@ -0,0 +1,41 @@
package config
import (
"os"
"runtime"
)
type ClientConfig struct {
configPath string
logFile string
daemonAddr string
}
// We are creating this package to extract utility functions from the cmd package
// reading and parsing the configurations for the client should be done here
func Config() *ClientConfig {
defaultConfigPath := "/etc/wiretrustee/config.json"
defaultLogFile := "/var/log/wiretrustee/client.log"
if runtime.GOOS == "windows" {
defaultConfigPath = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "config.json"
defaultLogFile = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "client.log"
}
defaultDaemonAddr := "unix:///var/run/wiretrustee.sock"
if runtime.GOOS == "windows" {
defaultDaemonAddr = "tcp://127.0.0.1:41731"
}
return &ClientConfig{
configPath: defaultConfigPath,
logFile: defaultLogFile,
daemonAddr: defaultDaemonAddr,
}
}
func (c *ClientConfig) DaemonAddr() string {
return c.daemonAddr
}
func (c *ClientConfig) LogFile() string {
return c.logFile
}

View File

@@ -6,7 +6,7 @@ a 3rd party open-source STUN/TURN service [Coturn](https://github.com/coturn/cot
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.
We focused on connectivity instead. It also offers an always free plan that should be ok for most users as its limits are high enough for most teams.
If you would like to learn more about the architecture please refer to the [Wiretrustee Architecture section](architecture.md).
@@ -17,10 +17,11 @@ If you would like to learn more about the architecture please refer to the [Wire
### Requirements
- Virtual machine offered by any cloud provider (e.g., AWS, DigitalOcean, Hetzner, Google Cloud, Azure ...).
- Any Linux OS.
- Any Unix OS.
- Docker Compose installed (see [Install Docker Compose](https://docs.docker.com/compose/install/)).
- Domain name pointing to the public IP address of your server.
- Open ports ```443, 33071, 33073, 10000, 3478``` (Dashboard, Management HTTP API, Management gRpc API, Signal gRpc, Coturn STUN/TURN respectively) on your server.
- Wiretrustee Open ports ```443, 33071, 33073, 10000``` (Dashboard, Management HTTP API, Management gRpc API, Signal gRpc) on your server.
- Coturn is used for relay using the STUN/TURN protocols. It requires a listening port, ```UDP 3478```, and range of ports,```UDP 49152-65535```, for dynamic relay connections.
- Maybe a cup of coffee or tea :)
### Step-by-step guide
@@ -41,7 +42,7 @@ For this tutorial we will be using domain ```test.wiretrustee.com``` which point
```
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.
To simplify the setup we have prepared a script to substitute required properties in the [turnserver.conf.tmpl](../infrastructure_files/turnserver.conf.tmpl),[docker-compose.yml.tmpl](../infrastructure_files/docker-compose.yml.tmpl) and [management.json.tmpl](../infrastructure_files/management.json.tmpl) files.
The [setup.env](../infrastructure_files/setup.env) file contains the following properties that have to be filled:
@@ -57,8 +58,9 @@ For this tutorial we will be using domain ```test.wiretrustee.com``` which point
# e.g. hello@mydomain.com
WIRETRUSTEE_LETSENCRYPT_EMAIL=""
```
> Other options are available, but they are automatically updated.
Please follow the steps to get the values.
Please follow the steps to get the values.
4. Configure ```WIRETRUSTEE_AUTH0_DOMAIN``` ```WIRETRUSTEE_AUTH0_CLIENT_ID``` ```WIRETRUSTEE_AUTH0_AUDIENCE``` properties.
@@ -94,3 +96,9 @@ For this tutorial we will be using domain ```test.wiretrustee.com``` which point
docker-compose logs management
docker-compose logs coturn
docker-compose logs dashboard
10. Once the server is running, you can access the dashboard by https://$WIRETRUSTEE_DOMAIN
11. Adding a peer will require you to enter the management URL by following the steps in the page https://$WIRETRUSTEE_DOMAIN/add-peer and in the 3rd step:
```shell
sudo wiretrustee up --setup-key <PASTE-SETUP-KEY> --management-url https://$WIRETRUSTEE_DOMAIN:33073
```

22
go.mod
View File

@@ -17,7 +17,7 @@ require (
github.com/spf13/cobra v1.3.0
github.com/spf13/pflag v1.0.5
github.com/vishvananda/netlink v1.1.0
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
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
@@ -28,8 +28,10 @@ require (
)
require (
github.com/getlantern/systray v1.2.0
github.com/magiconair/properties v1.8.5
github.com/rs/xid v1.3.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.7.0
)
@@ -37,6 +39,13 @@ require (
github.com/BurntSushi/toml v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/go-stack/stack v1.8.0 // 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
@@ -44,18 +53,19 @@ require (
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/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pion/dtls/v2 v2.1.2 // 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/transport v0.13.0 // indirect
github.com/pion/turn/v2 v2.0.7 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // 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-20211215060638-4ddde0e984e9 // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // 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
@@ -67,3 +77,5 @@ require (
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
honnef.co/go/tools v0.2.2 // indirect
)
replace github.com/pion/ice/v2 => github.com/wiretrustee/ice/v2 v2.1.21-0.20220218121004-dc81faead4bb

46
go.sum
View File

@@ -116,6 +116,20 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.2.0 h1:MsAdOcmOnm4V+r3HFONDszdZeoj7E3q2dEvsPdsxXtI=
github.com/getlantern/systray v1.2.0/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
@@ -128,6 +142,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -353,13 +368,13 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pion/dtls/v2 v2.0.12 h1:QMSvNht7FM/XDXij3Ic90SCbl5yL7kppeI4ghfF4in8=
github.com/pion/dtls/v2 v2.0.12/go.mod h1:5Pe3QJI0Ajsx+uCfxREeewGFlKYBzLrXe9ku7Y0oRXM=
github.com/pion/ice/v2 v2.1.17 h1:z7aBWgs85AEeRgtj0bHnCrShzaGnZ/RS4pMoRmbYxtY=
github.com/pion/ice/v2 v2.1.17/go.mod h1:M0MJ/tBR3IyDcaJv49hAiHEzaVBqWCV/MuWqIffBsrw=
github.com/pion/dtls/v2 v2.1.2 h1:22Q1Jk9L++Yo7BIf9130MonNPfPVb+YgdYLeyQotuAA=
github.com/pion/dtls/v2 v2.1.2/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
@@ -368,12 +383,11 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA=
github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw=
github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY=
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
github.com/pion/turn/v2 v2.0.7 h1:SZhc00WDovK6czaN1RSiHqbwANtIO6wfZQsU0m0KNE8=
github.com/pion/turn/v2 v2.0.7/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -409,6 +423,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
@@ -436,6 +452,8 @@ github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJ
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/wiretrustee/ice/v2 v2.1.21-0.20220218121004-dc81faead4bb h1:CU1/+CEeCPvYXgfAyqTJXSQSf6hW3wsWM6Dfz6HkHEQ=
github.com/wiretrustee/ice/v2 v2.1.21-0.20220218121004-dc81faead4bb/go.mod h1:XT1Nrb4OxbVFPffbQMbq4PaeEkpRLVzdphh3fjrw7DY=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -469,10 +487,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -570,8 +587,8 @@ golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211208012354-db4efeb81f4b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9 h1:kmreh1vGI63l2FxOAYS3Yv6ATsi7lSTuwNSVbGfJV9I=
golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -694,6 +711,7 @@ golang.org/x/sys v0.0.0-20211214234402-4825e8c3871d/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -9,6 +9,7 @@ import (
const (
DefaultMTU = 1280
DefaultWgPort = 51820
)
// WGIface represents a interface instance

View File

@@ -1,3 +1,4 @@
//go:build linux || windows
// +build linux windows
package iface

View File

@@ -1,3 +1,4 @@
//go:build darwin
// +build darwin
package iface

View File

@@ -1,7 +1,27 @@
#!/bin/bash
unset $(grep -v '^#' ./setup.env | sed -E 's/(.*)=.*/\1/' | xargs)
export $(grep -v '^#' ./setup.env | xargs)
source setup.env
if [[ "x-$WIRETRUSTEE_DOMAIN" == "x-" ]]
then
echo WIRETRUSTEE_DOMAIN is not set, please update your setup.env file
exit 1
fi
# local development or tests
if [[ $WIRETRUSTEE_DOMAIN == "localhost" || $WIRETRUSTEE_DOMAIN == "127.0.0.1" ]]
then
export WIRETRUSTEE_MGMT_API_ENDPOINT=http://$WIRETRUSTEE_DOMAIN:$WIRETRUSTEE_MGMT_API_PORT
unset WIRETRUSTEE_MGMT_API_CERT_FILE
unset WIRETRUSTEE_MGMT_API_CERT_KEY_FILE
fi
# if not provided, we generate a turn password
if [[ "x-$TURN_PASSWORD" == "x-" ]]
then
export TURN_PASSWORD=$(openssl rand -base64 32|sed 's/=//g')
fi
envsubst < docker-compose.yml.tmpl > docker-compose.yml
envsubst < management.json.tmpl > management.json
envsubst < turnserver.conf.tmpl > turnserver.conf

View File

@@ -11,19 +11,18 @@ services:
- 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
- WIRETRUSTEE_MGMT_API_ENDPOINT=$WIRETRUSTEE_MGMT_API_ENDPOINT
- NGINX_SSL_PORT=443
- LETSENCRYPT_DOMAIN=$WIRETRUSTEE_DOMAIN
- LETSENCRYPT_EMAIL=$WIRETRUSTEE_LETSENCRYPT_EMAIL
volumes:
- /var/lib/wiretrustee/dashboard/letsencrypt:/etc/letsencrypt/
- wiretrustee-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
@@ -37,12 +36,11 @@ services:
- dashboard
volumes:
- wiretrustee-mgmt:/var/lib/wiretrustee
- /var/lib/wiretrustee/dashboard/letsencrypt:/etc/letsencrypt:ro
- wiretrustee-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
- $WIRETRUSTEE_MGMT_API_PORT:33071 #API port
# # port and command for Let's Encrypt validation
# - 443:443
# command: ["--letsencrypt-domain", "$WIRETRUSTEE_DOMAIN", "--log-file", "console"]
@@ -50,7 +48,7 @@ services:
coturn:
image: coturn/coturn
restart: unless-stopped
domainname: <YOUR DOMAIN>
domainname: $WIRETRUSTEE_DOMAIN
volumes:
- ./turnserver.conf:/etc/turnserver.conf:ro
# - ./privkey.pem:/etc/coturn/private/privkey.pem:ro
@@ -58,4 +56,5 @@ services:
network_mode: host
volumes:
wiretrustee-mgmt:
wiretrustee-signal:
wiretrustee-signal:
wiretrustee-letsencrypt:

View File

@@ -12,8 +12,8 @@
{
"Proto": "udp",
"URI": "turn:$WIRETRUSTEE_DOMAIN:3478",
"Username": "",
"Password": null
"Username": "$TURN_USER",
"Password": "$TURN_PASSWORD"
}
],
"CredentialsTTL": "12h",
@@ -28,22 +28,14 @@
},
"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",
"Address": "0.0.0.0:$WIRETRUSTEE_MGMT_API_PORT",
"AuthIssuer": "https://$WIRETRUSTEE_AUTH0_DOMAIN/",
"AuthAudience": "$WIRETRUSTEE_AUTH0_AUDIENCE",
"AuthKeysLocation": "https://$WIRETRUSTEE_AUTH0_DOMAIN/.well-known/jwks.json"
"AuthKeysLocation": "https://$WIRETRUSTEE_AUTH0_DOMAIN/.well-known/jwks.json",
"CertFile":"$WIRETRUSTEE_MGMT_API_CERT_FILE",
"CertKey":"$WIRETRUSTEE_MGMT_API_CERT_KEY_FILE"
},
"IdpManagerConfig": {
"Manager": "none",
"Auth0ClientCredentials": {
"Audience": "<PASTE YOUR AUTH0 AUDIENCE HERE>",
"AuthIssuer": "<PASTE YOUR AUTH0 Auth Issuer HERE>",
"ClientId": "<PASTE YOUR AUTH0 Application Client ID HERE>",
"ClientSecret": "<PASTE YOUR AUTH0 Application Client Secret HERE>",
"GrantType": "client_credentials"
}
"Manager": "none"
}
}

View File

@@ -1,4 +1,6 @@
# e.g. app.mydomain.com
# Dashboard domain and auth0 configuration
# Dashboard domain. e.g. app.mydomain.com
WIRETRUSTEE_DOMAIN=""
# e.g. dev-24vkclam.us.auth0.com
WIRETRUSTEE_AUTH0_DOMAIN=""
@@ -8,3 +10,42 @@ WIRETRUSTEE_AUTH0_CLIENT_ID=""
WIRETRUSTEE_AUTH0_AUDIENCE=""
# e.g. hello@mydomain.com
WIRETRUSTEE_LETSENCRYPT_EMAIL=""
## From this point, most settings are being done automatically, but you can edit if you need some customization
# Management API
# Management API port
WIRETRUSTEE_MGMT_API_PORT=33071
# Management API endpoint address, used by the Dashboard
WIRETRUSTEE_MGMT_API_ENDPOINT=https://$WIRETRUSTEE_DOMAIN:$WIRETRUSTEE_MGMT_API_PORT
# Management Certficate file path. These are generated by the Dashboard container
WIRETRUSTEE_MGMT_API_CERT_FILE="/etc/letsencrypt/live/$WIRETRUSTEE_DOMAIN/fullchain.pem"
# Management Certficate key file path.
WIRETRUSTEE_MGMT_API_CERT_KEY_FILE="/etc/letsencrypt/live/$WIRETRUSTEE_DOMAIN/privkey.pem"
# Turn credentials
# User
TURN_USER=self
# Password. If empty, the configure.sh will generate one with openssl
TURN_PASSWORD=
# Min port
TURN_MIN_PORT=49152
# Max port
TURN_MAX_PORT=65535
# exports
export WIRETRUSTEE_DOMAIN
export WIRETRUSTEE_AUTH0_DOMAIN
export WIRETRUSTEE_AUTH0_CLIENT_ID
export WIRETRUSTEE_AUTH0_AUDIENCE
export WIRETRUSTEE_LETSENCRYPT_EMAIL
export WIRETRUSTEE_MGMT_API_PORT
export WIRETRUSTEE_MGMT_API_ENDPOINT
export WIRETRUSTEE_MGMT_API_CERT_FILE
export WIRETRUSTEE_MGMT_API_CERT_KEY_FILE
export TURN_USER
export TURN_PASSWORD
export TURN_MIN_PORT
export TURN_MAX_PORT

View File

@@ -154,12 +154,12 @@ tls-listening-port=5349
# Lower and upper bounds of the UDP relay endpoints:
# (default values are 49152 and 65535)
#
min-port=49152
max-port=65535
min-port=$TURN_MIN_PORT
max-port=$TURN_MAX_PORT
# Uncomment to run TURN server in 'normal' 'moderate' verbose mode.
# By default the verbose mode is off.
verbose
#verbose
# Uncomment to run TURN server in 'extra' verbose mode.
# This mode is very annoying and produces lots of output.
@@ -249,7 +249,7 @@ lt-cred-mech
#user=username1:key1
#user=username2:key2
# OR:
user=username1:password1
user=$TURN_USER:$TURN_PASSWORD
#user=username2:password2
#
# Keys must be generated by turnadmin utility. The key value depends

View File

@@ -16,6 +16,8 @@ import (
"github.com/wiretrustee/wiretrustee/management/proto"
mgmtProto "github.com/wiretrustee/wiretrustee/management/proto"
mgmt "github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/management/server/mock_server"
"github.com/wiretrustee/wiretrustee/util"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc"
@@ -25,7 +27,7 @@ import (
var tested *GrpcClient
var serverAddr string
var mgmtMockServer *mgmt.ManagementServiceServerMock
var mgmtMockServer *mock_server.ManagementServiceServerMock
var serverKey wgtypes.Key
const ValidKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB"
@@ -100,7 +102,7 @@ func startMockManagement(t *testing.T) (*grpc.Server, net.Listener) {
t.Fatal(err)
}
mgmtMockServer = &mgmt.ManagementServiceServerMock{
mgmtMockServer = &mock_server.ManagementServiceServerMock{
GetServerKeyFunc: func(context.Context, *proto.Empty) (*proto.ServerKeyResponse, error) {
response := &proto.ServerKeyResponse{
Key: serverKey.PublicKey().String(),

View File

@@ -29,7 +29,6 @@ type GrpcClient struct {
// NewClient creates a new client to Management service
func NewClient(ctx context.Context, addr string, ourPrivateKey wgtypes.Key, tlsEnabled bool) (*GrpcClient, error) {
transportOption := grpc.WithTransportCredentials(insecure.NewCredentials())
if tlsEnabled {
@@ -47,7 +46,6 @@ func NewClient(ctx context.Context, addr string, ourPrivateKey wgtypes.Key, tlsE
Time: 15 * time.Second,
Timeout: 10 * time.Second,
}))
if err != nil {
log.Errorf("failed creating connection to Management Service %v", err)
return nil, err
@@ -68,14 +66,14 @@ func (c *GrpcClient) Close() error {
return c.conn.Close()
}
//defaultBackoff is a basic backoff mechanism for general issues
// defaultBackoff is a basic backoff mechanism for general issues
func defaultBackoff(ctx context.Context) backoff.BackOff {
return backoff.WithContext(&backoff.ExponentialBackOff{
InitialInterval: 800 * time.Millisecond,
RandomizationFactor: backoff.DefaultRandomizationFactor,
Multiplier: backoff.DefaultMultiplier,
MaxInterval: 10 * time.Second,
MaxElapsedTime: 12 * time.Hour, //stop after 12 hours of trying, the error will be propagated to the general retry of the client
MaxElapsedTime: 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)
@@ -90,11 +88,9 @@ func (c *GrpcClient) ready() bool {
// 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 *GrpcClient) Sync(msgHandler func(msg *proto.SyncResponse) error) error {
var backOff = defaultBackoff(c.ctx)
backOff := defaultBackoff(c.ctx)
operation := func() error {
log.Debugf("management connection state %v", c.conn.GetState())
if !c.ready() {
@@ -215,7 +211,6 @@ func (c *GrpcClient) login(serverKey wgtypes.Key, req *proto.LoginRequest) (*pro
WgPubKey: c.key.PublicKey().String(),
Body: loginReq,
})
if err != nil {
return nil, err
}

View File

@@ -69,10 +69,15 @@ var (
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}
peersUpdateManager := server.NewPeersUpdateManager()
idpManager, err := idp.NewManager(*config.IdpManagerConfig)
if err != nil {
log.Fatalln("failed retrieving a new idp manager with err: ", err)
var idpManager idp.Manager
if config.IdpManagerConfig != nil {
idpManager, err = idp.NewManager(*config.IdpManagerConfig)
if err != nil {
log.Fatalln("failed retrieving a new idp manager with err: ", err)
}
}
accountManager := server.NewManager(store, peersUpdateManager, idpManager)
var opts []grpc.ServerOption

View File

@@ -4,13 +4,41 @@ import (
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/management/server/idp"
"github.com/wiretrustee/wiretrustee/management/server/jwtclaims"
"github.com/wiretrustee/wiretrustee/util"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"strings"
"sync"
)
type AccountManager struct {
const (
PublicCategory = "public"
PrivateCategory = "private"
UnknownCategory = "unknown"
)
type AccountManager interface {
GetOrCreateAccountByUser(userId, domain string) (*Account, error)
GetAccountByUser(userId string) (*Account, error)
AddSetupKey(accountId string, keyName string, keyType SetupKeyType, expiresIn *util.Duration) (*SetupKey, error)
RevokeSetupKey(accountId string, keyId string) (*SetupKey, error)
RenameSetupKey(accountId string, keyId string, newName string) (*SetupKey, error)
GetAccountById(accountId string) (*Account, error)
GetAccountByUserOrAccountId(userId, accountId, domain string) (*Account, error)
GetAccountWithAuthorizationClaims(claims jwtclaims.AuthorizationClaims) (*Account, error)
AccountExists(accountId string) (*bool, error)
AddAccount(accountId, userId, domain string) (*Account, error)
GetPeer(peerKey string) (*Peer, error)
MarkPeerConnected(peerKey string, connected bool) error
RenamePeer(accountId string, peerKey string, newName string) (*Peer, error)
DeletePeer(accountId string, peerKey string) (*Peer, error)
GetPeerByIP(accountId string, peerIP string) (*Peer, error)
GetNetworkMap(peerKey string) (*NetworkMap, error)
AddPeer(setupKey string, peer *Peer) (*Peer, error)
}
type DefaultAccountManager struct {
Store Store
// mutex to synchronise account operations (e.g. generating Peer IP address inside the Network)
mux sync.Mutex
@@ -22,12 +50,14 @@ type AccountManager struct {
type Account struct {
Id string
// User.Id it was created by
CreatedBy string
Domain string
SetupKeys map[string]*SetupKey
Network *Network
Peers map[string]*Peer
Users map[string]*User
CreatedBy string
Domain string
DomainCategory string
IsDomainPrimaryAccount bool
SetupKeys map[string]*SetupKey
Network *Network
Peers map[string]*Peer
Users map[string]*User
}
// NewAccount creates a new Account with a generated ID and generated default setup keys
@@ -62,9 +92,9 @@ func (a *Account) Copy() *Account {
}
}
// NewManager creates a new AccountManager with a provided Store
func NewManager(store Store, peersUpdateManager *PeersUpdateManager, idpManager idp.Manager) *AccountManager {
return &AccountManager{
// NewManager creates a new DefaultAccountManager with a provided Store
func NewManager(store Store, peersUpdateManager *PeersUpdateManager, idpManager idp.Manager) *DefaultAccountManager {
return &DefaultAccountManager{
Store: store,
mux: sync.Mutex{},
peersUpdateManager: peersUpdateManager,
@@ -73,7 +103,7 @@ func NewManager(store Store, peersUpdateManager *PeersUpdateManager, idpManager
}
//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 *util.Duration) (*SetupKey, error) {
func (am *DefaultAccountManager) AddSetupKey(accountId string, keyName string, keyType SetupKeyType, expiresIn *util.Duration) (*SetupKey, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -99,7 +129,7 @@ func (am *AccountManager) AddSetupKey(accountId string, keyName string, keyType
}
//RevokeSetupKey marks SetupKey as revoked - becomes not valid anymore
func (am *AccountManager) RevokeSetupKey(accountId string, keyId string) (*SetupKey, error) {
func (am *DefaultAccountManager) RevokeSetupKey(accountId string, keyId string) (*SetupKey, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -125,7 +155,7 @@ func (am *AccountManager) RevokeSetupKey(accountId string, keyId string) (*Setup
}
//RenameSetupKey renames existing setup key of the specified account.
func (am *AccountManager) RenameSetupKey(accountId string, keyId string, newName string) (*SetupKey, error) {
func (am *DefaultAccountManager) RenameSetupKey(accountId string, keyId string, newName string) (*SetupKey, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -151,7 +181,7 @@ func (am *AccountManager) RenameSetupKey(accountId string, keyId string, newName
}
//GetAccountById returns an existing account using its ID or error (NotFound) if doesn't exist
func (am *AccountManager) GetAccountById(accountId string) (*Account, error) {
func (am *DefaultAccountManager) GetAccountById(accountId string) (*Account, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -165,7 +195,7 @@ func (am *AccountManager) GetAccountById(accountId string) (*Account, error) {
//GetAccountByUserOrAccountId look for an account by user or account Id, if no account is provided and
// user id doesn't have an account associated with it, one account is created
func (am *AccountManager) GetAccountByUserOrAccountId(userId, accountId, domain string) (*Account, error) {
func (am *DefaultAccountManager) GetAccountByUserOrAccountId(userId, accountId, domain string) (*Account, error) {
if accountId != "" {
return am.GetAccountById(accountId)
@@ -174,12 +204,9 @@ func (am *AccountManager) GetAccountByUserOrAccountId(userId, accountId, domain
if err != nil {
return nil, status.Errorf(codes.NotFound, "account not found using user id: %s", userId)
}
// update idp manager app metadata
if am.idpManager != nil {
err = am.idpManager.UpdateUserAppMetadata(userId, idp.AppMetadata{WTAccountId: account.Id})
if err != nil {
return nil, status.Errorf(codes.Internal, "updating user's app metadata failed with: %v", err)
}
err = am.updateIDPMetadata(userId, account.Id)
if err != nil {
return nil, err
}
return account, nil
}
@@ -187,8 +214,139 @@ func (am *AccountManager) GetAccountByUserOrAccountId(userId, accountId, domain
return nil, status.Errorf(codes.NotFound, "no valid user or account Id provided")
}
// updateIDPMetadata update user's app metadata in idp manager
func (am *DefaultAccountManager) updateIDPMetadata(userId, accountID string) error {
if am.idpManager != nil {
err := am.idpManager.UpdateUserAppMetadata(userId, idp.AppMetadata{WTAccountId: accountID})
if err != nil {
return status.Errorf(codes.Internal, "updating user's app metadata failed with: %v", err)
}
}
return nil
}
// updateAccountDomainAttributes updates the account domain attributes and then, saves the account
func (am *DefaultAccountManager) updateAccountDomainAttributes(account *Account, claims jwtclaims.AuthorizationClaims, primaryDomain bool) error {
account.IsDomainPrimaryAccount = primaryDomain
account.Domain = strings.ToLower(claims.Domain)
account.DomainCategory = claims.DomainCategory
err := am.Store.SaveAccount(account)
if err != nil {
return status.Errorf(codes.Internal, "failed saving updated account")
}
return nil
}
// handleExistingUserAccount handles existing User accounts and update its domain attributes.
//
//
// If there is no primary domain account yet, we set the account as primary for the domain. Otherwise,
// we compare the account's ID with the domain account ID, and if they don't match, we set the account as
// non-primary account for the domain. We don't merge accounts at this stage, because of cases when a domain
// was previously unclassified or classified as public so N users that logged int that time, has they own account
// and peers that shouldn't be lost.
func (am *DefaultAccountManager) handleExistingUserAccount(existingAcc *Account, domainAcc *Account, claims jwtclaims.AuthorizationClaims) error {
var err error
if domainAcc == nil || existingAcc.Id != domainAcc.Id {
err = am.updateAccountDomainAttributes(existingAcc, claims, false)
if err != nil {
return err
}
}
// we should register the account ID to this user's metadata in our IDP manager
err = am.updateIDPMetadata(claims.UserId, existingAcc.Id)
if err != nil {
return err
}
return nil
}
// handleNewUserAccount validates if there is an existing primary account for the domain, if so it adds the new user to that account,
// otherwise it will create a new account and make it primary account for the domain.
func (am *DefaultAccountManager) handleNewUserAccount(domainAcc *Account, claims jwtclaims.AuthorizationClaims) (*Account, error) {
var (
account *Account
primaryAccount bool
)
lowerDomain := strings.ToLower(claims.Domain)
// if domain already has a primary account, add regular user
if domainAcc != nil {
account = domainAcc
account.Users[claims.UserId] = NewRegularUser(claims.UserId)
primaryAccount = false
} else {
account = NewAccount(claims.UserId, lowerDomain)
account.Users[claims.UserId] = NewAdminUser(claims.UserId)
primaryAccount = true
}
err := am.updateAccountDomainAttributes(account, claims, primaryAccount)
if err != nil {
return nil, err
}
err = am.updateIDPMetadata(claims.UserId, account.Id)
if err != nil {
return nil, err
}
return account, nil
}
// GetAccountWithAuthorizationClaims retrievs an account using JWT Claims.
// if domain is of the PrivateCategory category, it will evaluate
// if account is new, existing or if there is another account with the same domain
//
// Use cases:
//
// New user + New account + New domain -> create account, user role = admin (if private domain, index domain)
//
// New user + New account + Existing Private Domain -> add user to the existing account, user role = regular (not admin)
//
// New user + New account + Existing Public Domain -> create account, user role = admin
//
// Existing user + Existing account + Existing Domain -> Nothing changes (if private, index domain)
//
// Existing user + Existing account + Existing Indexed Domain -> Nothing changes
//
// Existing user + Existing account + Existing domain reclassified Domain as private -> Nothing changes (index domain)
func (am *DefaultAccountManager) GetAccountWithAuthorizationClaims(claims jwtclaims.AuthorizationClaims) (*Account, error) {
// if Account ID is part of the claims
// it means that we've already classified the domain and user has an account
if claims.DomainCategory != PrivateCategory || claims.AccountId != "" {
return am.GetAccountByUserOrAccountId(claims.UserId, claims.AccountId, claims.Domain)
}
am.mux.Lock()
defer am.mux.Unlock()
// We checked if the domain has a primary account already
domainAccount, err := am.Store.GetAccountByPrivateDomain(claims.Domain)
accStatus, _ := status.FromError(err)
if accStatus.Code() != codes.OK && accStatus.Code() != codes.NotFound {
return nil, err
}
account, err := am.Store.GetUserAccount(claims.UserId)
if err == nil {
err = am.handleExistingUserAccount(account, domainAccount, claims)
if err != nil {
return nil, err
}
return account, nil
} else if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
return am.handleNewUserAccount(domainAccount, claims)
} else {
// other error
return nil, err
}
}
//AccountExists checks whether account exists (returns true) or not (returns false)
func (am *AccountManager) AccountExists(accountId string) (*bool, error) {
func (am *DefaultAccountManager) AccountExists(accountId string) (*bool, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -208,7 +366,7 @@ func (am *AccountManager) AccountExists(accountId string) (*bool, error) {
}
// AddAccount generates a new Account with a provided accountId and userId, saves to the Store
func (am *AccountManager) AddAccount(accountId, userId, domain string) (*Account, error) {
func (am *DefaultAccountManager) AddAccount(accountId, userId, domain string) (*Account, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -217,7 +375,7 @@ func (am *AccountManager) AddAccount(accountId, userId, domain string) (*Account
}
func (am *AccountManager) createAccount(accountId, userId, domain string) (*Account, error) {
func (am *DefaultAccountManager) createAccount(accountId, userId, domain string) (*Account, error) {
account := newAccountWithId(accountId, userId, domain)
err := am.Store.SaveAccount(account)

View File

@@ -1,6 +1,8 @@
package server
import (
"github.com/stretchr/testify/require"
"github.com/wiretrustee/wiretrustee/management/server/jwtclaims"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"net"
"testing"
@@ -32,6 +34,154 @@ func TestAccountManager_GetOrCreateAccountByUser(t *testing.T) {
}
}
func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
type initUserParams jwtclaims.AuthorizationClaims
type test struct {
name string
inputClaims jwtclaims.AuthorizationClaims
inputInitUserParams initUserParams
inputUpdateAttrs bool
testingFunc require.ComparisonAssertionFunc
expectedMSG string
expectedUserRole UserRole
}
var (
publicDomain = "public.com"
privateDomain = "private.com"
unknownDomain = "unknown.com"
)
defaultInitAccount := initUserParams{
Domain: publicDomain,
UserId: "defaultUser",
}
testCase1 := test{
name: "New User With Public Domain",
inputClaims: jwtclaims.AuthorizationClaims{
Domain: publicDomain,
UserId: "pub-domain-user",
DomainCategory: PublicCategory,
},
inputInitUserParams: defaultInitAccount,
testingFunc: require.NotEqual,
expectedMSG: "account IDs shouldn't match",
expectedUserRole: UserRoleAdmin,
}
initUnknown := defaultInitAccount
initUnknown.DomainCategory = UnknownCategory
initUnknown.Domain = unknownDomain
testCase2 := test{
name: "New User With Unknown Domain",
inputClaims: jwtclaims.AuthorizationClaims{
Domain: unknownDomain,
UserId: "unknown-domain-user",
DomainCategory: UnknownCategory,
},
inputInitUserParams: initUnknown,
testingFunc: require.NotEqual,
expectedMSG: "account IDs shouldn't match",
expectedUserRole: UserRoleAdmin,
}
testCase3 := test{
name: "New User With Private Domain",
inputClaims: jwtclaims.AuthorizationClaims{
Domain: privateDomain,
UserId: "pvt-domain-user",
DomainCategory: PrivateCategory,
},
inputInitUserParams: defaultInitAccount,
testingFunc: require.NotEqual,
expectedMSG: "account IDs shouldn't match",
expectedUserRole: UserRoleAdmin,
}
privateInitAccount := defaultInitAccount
privateInitAccount.Domain = privateDomain
privateInitAccount.DomainCategory = PrivateCategory
testCase4 := test{
name: "New Regular User With Existing Private Domain",
inputClaims: jwtclaims.AuthorizationClaims{
Domain: privateDomain,
UserId: "pvt-domain-user",
DomainCategory: PrivateCategory,
},
inputUpdateAttrs: true,
inputInitUserParams: privateInitAccount,
testingFunc: require.Equal,
expectedMSG: "account IDs should match",
expectedUserRole: UserRoleUser,
}
testCase5 := test{
name: "Existing User With Existing Reclassified Private Domain",
inputClaims: jwtclaims.AuthorizationClaims{
Domain: defaultInitAccount.Domain,
UserId: defaultInitAccount.UserId,
DomainCategory: PrivateCategory,
},
inputInitUserParams: defaultInitAccount,
testingFunc: require.Equal,
expectedMSG: "account IDs should match",
expectedUserRole: UserRoleAdmin,
}
for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5} {
t.Run(testCase.name, func(t *testing.T) {
manager, err := createManager(t)
require.NoError(t, err, "unable to create account manager")
initAccount, err := manager.GetAccountByUserOrAccountId(testCase.inputInitUserParams.UserId, testCase.inputInitUserParams.AccountId, testCase.inputInitUserParams.Domain)
require.NoError(t, err, "create init user failed")
if testCase.inputUpdateAttrs {
err = manager.updateAccountDomainAttributes(initAccount, jwtclaims.AuthorizationClaims{UserId: testCase.inputInitUserParams.UserId, Domain: testCase.inputInitUserParams.Domain, DomainCategory: testCase.inputInitUserParams.DomainCategory}, true)
require.NoError(t, err, "update init user failed")
}
account, err := manager.GetAccountWithAuthorizationClaims(testCase.inputClaims)
require.NoError(t, err, "support function failed")
testCase.testingFunc(t, initAccount.Id, account.Id, testCase.expectedMSG)
require.EqualValues(t, testCase.expectedUserRole, account.Users[testCase.inputClaims.UserId].Role, "user role should match")
})
}
}
func TestAccountManager_PrivateAccount(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_SetOrUpdateDomain(t *testing.T) {
manager, err := createManager(t)
if err != nil {
@@ -320,7 +470,7 @@ func TestAccountManager_DeletePeer(t *testing.T) {
}
func createManager(t *testing.T) (*AccountManager, error) {
func createManager(t *testing.T) (*DefaultAccountManager, error) {
store, err := createStore(t)
if err != nil {
return nil, err

View File

@@ -17,10 +17,11 @@ const storeFileName = "store.json"
// FileStore represents an account storage backed by a file persisted to disk
type FileStore struct {
Accounts map[string]*Account
SetupKeyId2AccountId map[string]string `json:"-"`
PeerKeyId2AccountId map[string]string `json:"-"`
UserId2AccountId map[string]string `json:"-"`
Accounts map[string]*Account
SetupKeyId2AccountId map[string]string `json:"-"`
PeerKeyId2AccountId map[string]string `json:"-"`
UserId2AccountId map[string]string `json:"-"`
PrivateDomain2AccountId map[string]string `json:"-"`
// mutex to synchronise Store read/write operations
mux sync.Mutex `json:"-"`
@@ -42,12 +43,13 @@ func restore(file string) (*FileStore, error) {
if _, err := os.Stat(file); os.IsNotExist(err) {
// create a new FileStore if previously didn't exist (e.g. first run)
s := &FileStore{
Accounts: make(map[string]*Account),
mux: sync.Mutex{},
SetupKeyId2AccountId: make(map[string]string),
PeerKeyId2AccountId: make(map[string]string),
UserId2AccountId: make(map[string]string),
storeFile: file,
Accounts: make(map[string]*Account),
mux: sync.Mutex{},
SetupKeyId2AccountId: make(map[string]string),
PeerKeyId2AccountId: make(map[string]string),
UserId2AccountId: make(map[string]string),
PrivateDomain2AccountId: make(map[string]string),
storeFile: file,
}
err = s.persist(file)
@@ -68,6 +70,7 @@ func restore(file string) (*FileStore, error) {
store.SetupKeyId2AccountId = make(map[string]string)
store.PeerKeyId2AccountId = make(map[string]string)
store.UserId2AccountId = make(map[string]string)
store.PrivateDomain2AccountId = make(map[string]string)
for accountId, account := range store.Accounts {
for setupKeyId := range account.SetupKeys {
store.SetupKeyId2AccountId[strings.ToUpper(setupKeyId)] = accountId
@@ -78,6 +81,12 @@ func restore(file string) (*FileStore, error) {
for _, user := range account.Users {
store.UserId2AccountId[user.Id] = accountId
}
for _, user := range account.Users {
store.UserId2AccountId[user.Id] = accountId
}
if account.Domain != "" && account.DomainCategory == PrivateCategory && account.IsDomainPrimaryAccount {
store.PrivateDomain2AccountId[account.Domain] = accountId
}
}
return store, nil
@@ -178,6 +187,10 @@ func (s *FileStore) SaveAccount(account *Account) error {
s.UserId2AccountId[user.Id] = account.Id
}
if account.DomainCategory == PrivateCategory && account.IsDomainPrimaryAccount {
s.PrivateDomain2AccountId[account.Domain] = account.Id
}
err := s.persist(s.storeFile)
if err != nil {
return err
@@ -186,6 +199,21 @@ func (s *FileStore) SaveAccount(account *Account) error {
return nil
}
func (s *FileStore) GetAccountByPrivateDomain(domain string) (*Account, error) {
accountId, accountIdFound := s.PrivateDomain2AccountId[strings.ToLower(domain)]
if !accountIdFound {
return nil, status.Errorf(codes.NotFound, "provided domain is not registered or is not private")
}
account, err := s.GetAccount(accountId)
if err != nil {
return nil, err
}
return account, nil
}
func (s *FileStore) GetAccountBySetupKey(setupKey string) (*Account, error) {
accountId, accountIdFound := s.SetupKeyId2AccountId[strings.ToUpper(setupKey)]

View File

@@ -1,6 +1,7 @@
package server
import (
"github.com/stretchr/testify/require"
"github.com/wiretrustee/wiretrustee/util"
"net"
"path/filepath"
@@ -131,34 +132,45 @@ func TestRestore(t *testing.T) {
}
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")
require.NotNil(t, account, "failed to restore a FileStore file - missing account bf1c8084-ba50-4ce7-9439-34653001fc3b")
require.NotNil(t, account.Users["edafee4e-63fb-11ec-90d6-0242ac120003"], "failed to restore a FileStore file - missing Account User edafee4e-63fb-11ec-90d6-0242ac120003")
require.NotNil(t, account.Users["f4f6d672-63fb-11ec-90d6-0242ac120003"], "failed to restore a FileStore file - missing Account User f4f6d672-63fb-11ec-90d6-0242ac120003")
require.NotNil(t, account.Network, "failed to restore a FileStore file - missing Account Network")
require.NotNil(t, account.SetupKeys["A2C8E62B-38F5-4553-B31E-DD66C696CEBB"], "failed to restore a FileStore file - missing Account SetupKey A2C8E62B-38F5-4553-B31E-DD66C696CEBB")
require.Len(t, store.UserId2AccountId, 2, "failed to restore a FileStore wrong UserId2AccountId mapping length")
require.Len(t, store.SetupKeyId2AccountId, 1, "failed to restore a FileStore wrong SetupKeyId2AccountId mapping length")
require.Len(t, store.PrivateDomain2AccountId, 1, "failed to restore a FileStore wrong PrivateDomain2AccountId mapping length")
}
func TestGetAccountByPrivateDomain(t *testing.T) {
storeDir := t.TempDir()
err := util.CopyFileContents("testdata/store.json", filepath.Join(storeDir, "store.json"))
if err != nil {
t.Fatal(err)
}
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")
store, err := NewStore(storeDir)
if err != nil {
return
}
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")
}
existingDomain := "test.com"
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")
}
account, err := store.GetAccountByPrivateDomain(existingDomain)
require.NoError(t, err, "should found account")
require.Equal(t, existingDomain, account.Domain, "domains should match")
_, err = store.GetAccountByPrivateDomain("missing-domain.com")
require.Error(t, err, "should return error on domain lookup")
}
func newStore(t *testing.T) *FileStore {

View File

@@ -16,7 +16,7 @@ import (
// Server an instance of a Management server
type Server struct {
accountManager *AccountManager
accountManager AccountManager
wgKey wgtypes.Key
proto.UnimplementedManagementServiceServer
peersUpdateManager *PeersUpdateManager
@@ -28,7 +28,7 @@ type Server struct {
const AllowedIPsFormat = "%s/32"
// NewServer creates a new Management server
func NewServer(config *Config, accountManager *AccountManager, peersUpdateManager *PeersUpdateManager, turnCredentialsManager TURNCredentialsManager) (*Server, error) {
func NewServer(config *Config, accountManager AccountManager, peersUpdateManager *PeersUpdateManager, turnCredentialsManager TURNCredentialsManager) (*Server, error) {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
@@ -158,7 +158,7 @@ func (s *Server) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest) (*Pe
return nil, status.Errorf(codes.NotFound, "provided setup key doesn't exists")
}
//todo move to AccountManager the code below
//todo move to DefaultAccountManager the code below
networkMap, err := s.accountManager.GetNetworkMap(peer.Key)
if err != nil {
return nil, status.Error(codes.Internal, "internal server error")

View File

@@ -3,17 +3,20 @@ package handler
import (
"encoding/json"
"fmt"
"github.com/wiretrustee/wiretrustee/management/server/jwtclaims"
"net/http"
"time"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/management/server"
"net/http"
"time"
)
//Peers is a handler that returns peers of the account
type Peers struct {
accountManager *server.AccountManager
accountManager server.AccountManager
authAudience string
jwtExtractor jwtclaims.ClaimsExtractor
}
//PeerResponse is a response sent to the client
@@ -31,10 +34,11 @@ type PeerRequest struct {
Name string
}
func NewPeers(accountManager *server.AccountManager, authAudience string) *Peers {
func NewPeers(accountManager server.AccountManager, authAudience string) *Peers {
return &Peers{
accountManager: accountManager,
authAudience: authAudience,
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
}
}
@@ -54,6 +58,7 @@ func (h *Peers) updatePeer(accountId string, peer *server.Peer, w http.ResponseW
}
writeJSONObject(w, toPeerResponse(peer))
}
func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseWriter, r *http.Request) {
_, err := h.accountManager.DeletePeer(accountId, peer.Key)
if err != nil {
@@ -65,9 +70,9 @@ func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseW
}
func (h *Peers) getPeerAccount(r *http.Request) (*server.Account, error) {
jwtClaims := extractClaimsFromRequestContext(r, h.authAudience)
jwtClaims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, err := h.accountManager.GetAccountByUserOrAccountId(jwtClaims.UserId, jwtClaims.AccountId, jwtClaims.Domain)
account, err := h.accountManager.GetAccountWithAuthorizationClaims(jwtClaims)
if err != nil {
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
}

View File

@@ -0,0 +1,108 @@
package handler
import (
"encoding/json"
"github.com/wiretrustee/wiretrustee/management/server/jwtclaims"
"io"
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/magiconair/properties/assert"
"github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/management/server/mock_server"
)
func initTestMetaData(peer ...*server.Peer) *Peers {
return &Peers{
accountManager: &mock_server.MockAccountManager{
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
return &server.Account{
Id: claims.AccountId,
Domain: "hotmail.com",
Peers: map[string]*server.Peer{
"test_peer": peer[0],
},
}, nil
},
},
authAudience: "",
jwtExtractor: jwtclaims.ClaimsExtractor{
ExtractClaimsFromRequestContext: func(r *http.Request, authAudiance string) jwtclaims.AuthorizationClaims {
return jwtclaims.AuthorizationClaims{
UserId: "test_user",
Domain: "hotmail.com",
AccountId: "test_id",
}
},
},
}
}
// Tests the GetPeers endpoint reachable in the route /api/peers
// Use the metadata generated by initTestMetaData() to check for values
func TestGetPeers(t *testing.T) {
var tt = []struct {
name string
expectedStatus int
requestType string
requestPath string
requestBody io.Reader
}{
{name: "GetPeersMetaData", requestType: http.MethodGet, requestPath: "/api/peers/", expectedStatus: http.StatusOK},
}
rr := httptest.NewRecorder()
peer := &server.Peer{
Key: "key",
SetupKey: "setupkey",
IP: net.ParseIP("100.64.0.1"),
Status: &server.PeerStatus{},
Name: "PeerName",
Meta: server.PeerSystemMeta{
Hostname: "hostname",
GoOS: "GoOS",
Kernel: "kernel",
Core: "core",
Platform: "platform",
OS: "OS",
WtVersion: "development",
},
}
p := initTestMetaData(peer)
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
p.GetPeers(rr, req)
res := rr.Result()
defer res.Body.Close()
if status := rr.Code; status != tc.expectedStatus {
t.Fatalf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
content, err := io.ReadAll(res.Body)
if err != nil {
t.Fatalf("I don't know what I expected; %v", err)
}
respBody := []*PeerResponse{}
err = json.Unmarshal(content, &respBody)
if err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
got := respBody[0]
assert.Equal(t, got.Name, peer.Name)
assert.Equal(t, got.Version, peer.Meta.WtVersion)
assert.Equal(t, got.IP, peer.IP.String())
assert.Equal(t, got.OS, "OS core")
})
}
}

View File

@@ -3,19 +3,21 @@ package handler
import (
"encoding/json"
"fmt"
"github.com/wiretrustee/wiretrustee/management/server/jwtclaims"
"net/http"
"time"
"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"
"time"
)
// SetupKeys is a handler that returns a list of setup keys of the account
type SetupKeys struct {
accountManager *server.AccountManager
accountManager server.AccountManager
authAudience string
}
@@ -41,7 +43,7 @@ type SetupKeyRequest struct {
Revoked bool
}
func NewSetupKeysHandler(accountManager *server.AccountManager, authAudience string) *SetupKeys {
func NewSetupKeysHandler(accountManager server.AccountManager, authAudience string) *SetupKeys {
return &SetupKeys{
accountManager: accountManager,
authAudience: authAudience,
@@ -121,9 +123,10 @@ func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.R
}
func (h *SetupKeys) getSetupKeyAccount(r *http.Request) (*server.Account, error) {
jwtClaims := extractClaimsFromRequestContext(r, h.authAudience)
extractor := jwtclaims.NewClaimsExtractor(nil)
jwtClaims := extractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, err := h.accountManager.GetAccountByUserOrAccountId(jwtClaims.UserId, jwtClaims.AccountId, jwtClaims.Domain)
account, err := h.accountManager.GetAccountWithAuthorizationClaims(jwtClaims)
if err != nil {
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
}

View File

@@ -3,38 +3,13 @@ package handler
import (
"encoding/json"
"errors"
"github.com/golang-jwt/jwt"
"net/http"
"time"
)
// JWTClaims stores information from JWTs
type JWTClaims struct {
UserId string
AccountId string
Domain string
}
// extractClaimsFromRequestContext extracts claims from the request context previously filled by the JWT token (after auth)
func extractClaimsFromRequestContext(r *http.Request, authAudiance string) JWTClaims {
token := r.Context().Value("user").(*jwt.Token)
claims := token.Claims.(jwt.MapClaims)
jwtClaims := JWTClaims{}
jwtClaims.UserId = claims["sub"].(string)
accountIdClaim, ok := claims[authAudiance+"wt_account_id"]
if ok {
jwtClaims.AccountId = accountIdClaim.(string)
}
domainClaim, ok := claims[authAudiance+"wt_user_domain"]
if ok {
jwtClaims.Domain = domainClaim.(string)
}
return jwtClaims
}
//writeJSONObject simply writes object to the HTTP reponse in JSON format
func writeJSONObject(w http.ResponseWriter, obj interface{}) {
w.WriteHeader(200)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
err := json.NewEncoder(w).Encode(obj)
if err != nil {

View File

@@ -3,6 +3,9 @@ package http
import (
"context"
"crypto/tls"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/rs/cors"
log "github.com/sirupsen/logrus"
@@ -10,8 +13,6 @@ import (
"github.com/wiretrustee/wiretrustee/management/server/http/handler"
"github.com/wiretrustee/wiretrustee/management/server/http/middleware"
"golang.org/x/crypto/acme/autocert"
"net/http"
"time"
)
type Server struct {
@@ -19,12 +20,12 @@ type Server struct {
config *s.HttpServerConfig
certManager *autocert.Manager
tlsConfig *tls.Config
accountManager *s.AccountManager
accountManager s.AccountManager
}
// NewHttpsServer creates a new HTTPs server (with HTTPS support) and a certManager that is responsible for generating and renewing Let's Encrypt certificate
// The listening address will be :443 no matter what was specified in s.HttpServerConfig.Address
func NewHttpsServer(config *s.HttpServerConfig, certManager *autocert.Manager, accountManager *s.AccountManager) *Server {
func NewHttpsServer(config *s.HttpServerConfig, certManager *autocert.Manager, accountManager s.AccountManager) *Server {
server := &http.Server{
Addr: config.Address,
WriteTimeout: time.Second * 15,
@@ -36,7 +37,7 @@ func NewHttpsServer(config *s.HttpServerConfig, certManager *autocert.Manager, a
// 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 {
func NewHttpsServerWithTLSConfig(config *s.HttpServerConfig, tlsConfig *tls.Config, accountManager s.AccountManager) *Server {
server := &http.Server{
Addr: config.Address,
WriteTimeout: time.Second * 15,
@@ -47,7 +48,7 @@ func NewHttpsServerWithTLSConfig(config *s.HttpServerConfig, tlsConfig *tls.Conf
}
// NewHttpServer creates a new HTTP server (without HTTPS)
func NewHttpServer(config *s.HttpServerConfig, accountManager *s.AccountManager) *Server {
func NewHttpServer(config *s.HttpServerConfig, accountManager s.AccountManager) *Server {
return NewHttpsServer(config, nil, accountManager)
}

View File

@@ -3,13 +3,14 @@ package idp
import (
"encoding/json"
"fmt"
"github.com/golang-jwt/jwt"
"github.com/stretchr/testify/assert"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt"
"github.com/stretchr/testify/assert"
)
type mockHTTPClient struct {

View File

@@ -0,0 +1,9 @@
package jwtclaims
// AuthorizationClaims stores authorization information from JWTs
type AuthorizationClaims struct {
UserId string
AccountId string
Domain string
DomainCategory string
}

View File

@@ -0,0 +1,56 @@
package jwtclaims
import (
"github.com/golang-jwt/jwt"
"net/http"
)
const (
TokenUserProperty = "user"
AccountIDSuffix = "wt_account_id"
DomainIDSuffix = "wt_account_domain"
DomainCategorySuffix = "wt_account_domain_category"
UserIDClaim = "sub"
)
// Extract function type
type ExtractClaims func(r *http.Request, authAudiance string) AuthorizationClaims
// ClaimsExtractor struct that holds the extract function
type ClaimsExtractor struct {
ExtractClaimsFromRequestContext ExtractClaims
}
// NewClaimsExtractor returns an extractor, and if provided with a function with ExtractClaims signature,
// then it will use that logic. Uses ExtractClaimsFromRequestContext by default
func NewClaimsExtractor(e ExtractClaims) *ClaimsExtractor {
var extractFunc ExtractClaims
if extractFunc = e; extractFunc == nil {
extractFunc = ExtractClaimsFromRequestContext
}
return &ClaimsExtractor{
ExtractClaimsFromRequestContext: extractFunc,
}
}
// ExtractClaimsFromRequestContext extracts claims from the request context previously filled by the JWT token (after auth)
func ExtractClaimsFromRequestContext(r *http.Request, authAudiance string) AuthorizationClaims {
token := r.Context().Value(TokenUserProperty).(*jwt.Token)
claims := token.Claims.(jwt.MapClaims)
jwtClaims := AuthorizationClaims{}
jwtClaims.UserId = claims[UserIDClaim].(string)
accountIdClaim, ok := claims[authAudiance+AccountIDSuffix]
if ok {
jwtClaims.AccountId = accountIdClaim.(string)
}
domainClaim, ok := claims[authAudiance+DomainIDSuffix]
if ok {
jwtClaims.Domain = domainClaim.(string)
}
domainCategoryClaim, ok := claims[authAudiance+DomainCategorySuffix]
if ok {
jwtClaims.DomainCategory = domainCategoryClaim.(string)
}
return jwtClaims
}

View File

@@ -0,0 +1,110 @@
package jwtclaims
import (
"context"
"github.com/golang-jwt/jwt"
"github.com/stretchr/testify/require"
"net/http"
"testing"
)
func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audiance string) *http.Request {
claimMaps := jwt.MapClaims{}
if claims.UserId != "" {
claimMaps[UserIDClaim] = claims.UserId
}
if claims.AccountId != "" {
claimMaps[audiance+AccountIDSuffix] = claims.AccountId
}
if claims.Domain != "" {
claimMaps[audiance+DomainIDSuffix] = claims.Domain
}
if claims.DomainCategory != "" {
claimMaps[audiance+DomainCategorySuffix] = claims.DomainCategory
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
r, err := http.NewRequest(http.MethodGet, "http://localhost", nil)
require.NoError(t, err, "creating testing request failed")
testRequest := r.WithContext(context.WithValue(r.Context(), TokenUserProperty, token)) //nolint
return testRequest
}
func TestExtractClaimsFromRequestContext(t *testing.T) {
type test struct {
name string
inputAuthorizationClaims AuthorizationClaims
inputAudiance string
testingFunc require.ComparisonAssertionFunc
expectedMSG string
}
testCase1 := test{
name: "All Claim Fields",
inputAudiance: "https://login/",
inputAuthorizationClaims: AuthorizationClaims{
UserId: "test",
Domain: "test.com",
AccountId: "testAcc",
DomainCategory: "public",
},
testingFunc: require.EqualValues,
expectedMSG: "extracted claims should match input claims",
}
testCase2 := test{
name: "Domain Is Empty",
inputAudiance: "https://login/",
inputAuthorizationClaims: AuthorizationClaims{
UserId: "test",
AccountId: "testAcc",
},
testingFunc: require.EqualValues,
expectedMSG: "extracted claims should match input claims",
}
testCase3 := test{
name: "Account ID Is Empty",
inputAudiance: "https://login/",
inputAuthorizationClaims: AuthorizationClaims{
UserId: "test",
Domain: "test.com",
},
testingFunc: require.EqualValues,
expectedMSG: "extracted claims should match input claims",
}
testCase4 := test{
name: "Category Is Empty",
inputAudiance: "https://login/",
inputAuthorizationClaims: AuthorizationClaims{
UserId: "test",
Domain: "test.com",
AccountId: "testAcc",
},
testingFunc: require.EqualValues,
expectedMSG: "extracted claims should match input claims",
}
testCase5 := test{
name: "Only User ID Is set",
inputAudiance: "https://login/",
inputAuthorizationClaims: AuthorizationClaims{
UserId: "test",
},
testingFunc: require.EqualValues,
expectedMSG: "extracted claims should match input claims",
}
for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5} {
t.Run(testCase.name, func(t *testing.T) {
request := newTestRequestWithJWT(t, testCase.inputAuthorizationClaims, testCase.inputAudiance)
extractedClaims := ExtractClaimsFromRequestContext(request, testCase.inputAudiance)
testCase.testingFunc(t, testCase.inputAuthorizationClaims, extractedClaims, testCase.expectedMSG)
})
}
}

View File

@@ -0,0 +1,148 @@
package mock_server
import (
"github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/management/server/jwtclaims"
"github.com/wiretrustee/wiretrustee/util"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type MockAccountManager struct {
GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error)
GetAccountByUserFunc func(userId string) (*server.Account, error)
AddSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType, expiresIn *util.Duration) (*server.SetupKey, error)
RevokeSetupKeyFunc func(accountId string, keyId string) (*server.SetupKey, error)
RenameSetupKeyFunc func(accountId string, keyId string, newName string) (*server.SetupKey, error)
GetAccountByIdFunc func(accountId string) (*server.Account, error)
GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error)
GetAccountWithAuthorizationClaimsFunc func(claims jwtclaims.AuthorizationClaims) (*server.Account, error)
AccountExistsFunc func(accountId string) (*bool, error)
AddAccountFunc func(accountId, userId, domain string) (*server.Account, error)
GetPeerFunc func(peerKey string) (*server.Peer, error)
MarkPeerConnectedFunc func(peerKey string, connected bool) error
RenamePeerFunc func(accountId string, peerKey string, newName string) (*server.Peer, error)
DeletePeerFunc func(accountId string, peerKey string) (*server.Peer, error)
GetPeerByIPFunc func(accountId string, peerIP string) (*server.Peer, error)
GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error)
AddPeerFunc func(setupKey string, peer *server.Peer) (*server.Peer, error)
}
func (am *MockAccountManager) GetOrCreateAccountByUser(userId, domain string) (*server.Account, error) {
if am.GetOrCreateAccountByUserFunc != nil {
return am.GetOrCreateAccountByUserFunc(userId, domain)
}
return nil, status.Errorf(codes.Unimplemented, "method GetOrCreateAccountByUser not implemented")
}
func (am *MockAccountManager) GetAccountByUser(userId string) (*server.Account, error) {
if am.GetAccountByUserFunc != nil {
return am.GetAccountByUserFunc(userId)
}
return nil, status.Errorf(codes.Unimplemented, "method GetAccountByUser not implemented")
}
func (am *MockAccountManager) AddSetupKey(accountId string, keyName string, keyType server.SetupKeyType, expiresIn *util.Duration) (*server.SetupKey, error) {
if am.AddSetupKeyFunc != nil {
return am.AddSetupKeyFunc(accountId, keyName, keyType, expiresIn)
}
return nil, status.Errorf(codes.Unimplemented, "method AddSetupKey not implemented")
}
func (am *MockAccountManager) RevokeSetupKey(accountId string, keyId string) (*server.SetupKey, error) {
if am.RevokeSetupKeyFunc != nil {
return am.RevokeSetupKeyFunc(accountId, keyId)
}
return nil, status.Errorf(codes.Unimplemented, "method RevokeSetupKey not implemented")
}
func (am *MockAccountManager) RenameSetupKey(accountId string, keyId string, newName string) (*server.SetupKey, error) {
if am.RenameSetupKeyFunc != nil {
return am.RenameSetupKeyFunc(accountId, keyId, newName)
}
return nil, status.Errorf(codes.Unimplemented, "method RenameSetupKey not implemented")
}
func (am *MockAccountManager) GetAccountById(accountId string) (*server.Account, error) {
if am.GetAccountByIdFunc != nil {
return am.GetAccountByIdFunc(accountId)
}
return nil, status.Errorf(codes.Unimplemented, "method GetAccountById not implemented")
}
func (am *MockAccountManager) GetAccountByUserOrAccountId(userId, accountId, domain string) (*server.Account, error) {
if am.GetAccountByUserOrAccountIdFunc != nil {
return am.GetAccountByUserOrAccountIdFunc(userId, accountId, domain)
}
return nil, status.Errorf(codes.Unimplemented, "method GetAccountByUserOrAccountId not implemented")
}
func (am *MockAccountManager) GetAccountWithAuthorizationClaims(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
if am.GetAccountWithAuthorizationClaimsFunc != nil {
return am.GetAccountWithAuthorizationClaimsFunc(claims)
}
return nil, status.Errorf(codes.Unimplemented, "method GetAccountWithAuthorizationClaims not implemented")
}
func (am *MockAccountManager) AccountExists(accountId string) (*bool, error) {
if am.AccountExistsFunc != nil {
return am.AccountExistsFunc(accountId)
}
return nil, status.Errorf(codes.Unimplemented, "method AccountExists not implemented")
}
func (am *MockAccountManager) AddAccount(accountId, userId, domain string) (*server.Account, error) {
if am.AddAccountFunc != nil {
return am.AddAccountFunc(accountId, userId, domain)
}
return nil, status.Errorf(codes.Unimplemented, "method AddAccount not implemented")
}
func (am *MockAccountManager) GetPeer(peerKey string) (*server.Peer, error) {
if am.GetPeerFunc != nil {
return am.GetPeerFunc(peerKey)
}
return nil, status.Errorf(codes.Unimplemented, "method GetPeer not implemented")
}
func (am *MockAccountManager) MarkPeerConnected(peerKey string, connected bool) error {
if am.MarkPeerConnectedFunc != nil {
return am.MarkPeerConnectedFunc(peerKey, connected)
}
return status.Errorf(codes.Unimplemented, "method MarkPeerConnected not implemented")
}
func (am *MockAccountManager) RenamePeer(accountId string, peerKey string, newName string) (*server.Peer, error) {
if am.RenamePeerFunc != nil {
return am.RenamePeerFunc(accountId, peerKey, newName)
}
return nil, status.Errorf(codes.Unimplemented, "method RenamePeer not implemented")
}
func (am *MockAccountManager) DeletePeer(accountId string, peerKey string) (*server.Peer, error) {
if am.DeletePeerFunc != nil {
return am.DeletePeerFunc(accountId, peerKey)
}
return nil, status.Errorf(codes.Unimplemented, "method DeletePeer not implemented")
}
func (am *MockAccountManager) GetPeerByIP(accountId string, peerIP string) (*server.Peer, error) {
if am.GetPeerByIPFunc != nil {
return am.GetPeerByIPFunc(accountId, peerIP)
}
return nil, status.Errorf(codes.Unimplemented, "method GetPeerByIP not implemented")
}
func (am *MockAccountManager) GetNetworkMap(peerKey string) (*server.NetworkMap, error) {
if am.GetNetworkMapFunc != nil {
return am.GetNetworkMapFunc(peerKey)
}
return nil, status.Errorf(codes.Unimplemented, "method GetNetworkMap not implemented")
}
func (am *MockAccountManager) AddPeer(setupKey string, peer *server.Peer) (*server.Peer, error) {
if am.AddPeerFunc != nil {
return am.AddPeerFunc(setupKey, peer)
}
return nil, status.Errorf(codes.Unimplemented, "method AddPeer not implemented")
}

View File

@@ -1,4 +1,4 @@
package server
package mock_server
import (
"context"

View File

@@ -56,7 +56,7 @@ func (p *Peer) Copy() *Peer {
}
//GetPeer returns a peer from a Store
func (am *AccountManager) GetPeer(peerKey string) (*Peer, error) {
func (am *DefaultAccountManager) GetPeer(peerKey string) (*Peer, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -69,7 +69,7 @@ func (am *AccountManager) GetPeer(peerKey string) (*Peer, error) {
}
//MarkPeerConnected marks peer as connected (true) or disconnected (false)
func (am *AccountManager) MarkPeerConnected(peerKey string, connected bool) error {
func (am *DefaultAccountManager) MarkPeerConnected(peerKey string, connected bool) error {
am.mux.Lock()
defer am.mux.Unlock()
@@ -94,7 +94,7 @@ func (am *AccountManager) MarkPeerConnected(peerKey string, connected bool) erro
}
//RenamePeer changes peer's name
func (am *AccountManager) RenamePeer(accountId string, peerKey string, newName string) (*Peer, error) {
func (am *DefaultAccountManager) RenamePeer(accountId string, peerKey string, newName string) (*Peer, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -114,7 +114,7 @@ func (am *AccountManager) RenamePeer(accountId string, peerKey string, newName s
}
//DeletePeer removes peer from the account by it's IP
func (am *AccountManager) DeletePeer(accountId string, peerKey string) (*Peer, error) {
func (am *DefaultAccountManager) DeletePeer(accountId string, peerKey string) (*Peer, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -188,7 +188,7 @@ func (am *AccountManager) DeletePeer(accountId string, peerKey string) (*Peer, e
}
//GetPeerByIP returns peer by it's IP
func (am *AccountManager) GetPeerByIP(accountId string, peerIP string) (*Peer, error) {
func (am *DefaultAccountManager) GetPeerByIP(accountId string, peerIP string) (*Peer, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -207,7 +207,7 @@ func (am *AccountManager) GetPeerByIP(accountId string, peerIP string) (*Peer, e
}
// GetNetworkMap returns Network map for a given peer (omits original peer from the Peers result)
func (am *AccountManager) GetNetworkMap(peerKey string) (*NetworkMap, error) {
func (am *DefaultAccountManager) GetNetworkMap(peerKey string) (*NetworkMap, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -235,7 +235,7 @@ func (am *AccountManager) GetNetworkMap(peerKey string) (*NetworkMap, error) {
// 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).
// The peer property is just a placeholder for the Peer properties to pass further
func (am *AccountManager) AddPeer(setupKey string, peer *Peer) (*Peer, error) {
func (am *DefaultAccountManager) AddPeer(setupKey string, peer *Peer) (*Peer, error) {
am.mux.Lock()
defer am.mux.Unlock()

View File

@@ -9,5 +9,6 @@ type Store interface {
GetAccountPeers(accountId string) ([]*Peer, error)
GetPeerAccount(peerKey string) (*Account, error)
GetAccountBySetupKey(setupKey string) (*Account, error)
GetAccountByPrivateDomain(domain string) (*Account, error)
SaveAccount(account *Account) error
}

View File

@@ -2,6 +2,9 @@
"Accounts": {
"bf1c8084-ba50-4ce7-9439-34653001fc3b": {
"Id": "bf1c8084-ba50-4ce7-9439-34653001fc3b",
"Domain": "test.com",
"DomainCategory": "private",
"IsDomainPrimaryAccount": true,
"SetupKeys": {
"A2C8E62B-38F5-4553-B31E-DD66C696CEBB": {
"Key": "A2C8E62B-38F5-4553-B31E-DD66C696CEBB",

View File

@@ -3,6 +3,7 @@ package server
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"strings"
)
const (
@@ -34,20 +35,27 @@ func NewUser(id string, role UserRole) *User {
}
}
// NewRegularUser creates a new user with role UserRoleAdmin
func NewRegularUser(id string) *User {
return NewUser(id, UserRoleUser)
}
// 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, domain string) (*Account, error) {
func (am *DefaultAccountManager) GetOrCreateAccountByUser(userId, domain string) (*Account, error) {
am.mux.Lock()
defer am.mux.Unlock()
lowerDomain := strings.ToLower(domain)
account, err := am.Store.GetUserAccount(userId)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
account = NewAccount(userId, domain)
account = NewAccount(userId, lowerDomain)
account.Users[userId] = NewAdminUser(userId)
err = am.Store.SaveAccount(account)
if err != nil {
@@ -59,8 +67,8 @@ func (am *AccountManager) GetOrCreateAccountByUser(userId, domain string) (*Acco
}
}
if account.Domain != domain {
account.Domain = domain
if account.Domain != lowerDomain {
account.Domain = lowerDomain
err = am.Store.SaveAccount(account)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed updating account with domain")
@@ -71,7 +79,7 @@ func (am *AccountManager) GetOrCreateAccountByUser(userId, domain string) (*Acco
}
// 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) {
func (am *DefaultAccountManager) GetAccountByUser(userId string) (*Account, error) {
am.mux.Lock()
defer am.mux.Unlock()

View File

@@ -12,7 +12,7 @@ import (
// The output JSON is pretty-formatted
func WriteJson(file string, obj interface{}) error {
configDir := filepath.Dir(file)
configDir, configFileName := filepath.Split(file)
err := os.MkdirAll(configDir, 0750)
if err != nil {
return err
@@ -24,7 +24,31 @@ func WriteJson(file string, obj interface{}) error {
return err
}
err = ioutil.WriteFile(file, bs, 0600)
tempFile, err := ioutil.TempFile(configDir, ".*"+configFileName)
if err != nil {
return err
}
tempFileName := tempFile.Name()
// closing file ops as windows doesn't allow to move it
err = tempFile.Close()
if err != nil {
return err
}
defer func() {
_, err = os.Stat(tempFileName)
if err == nil {
os.Remove(tempFileName)
}
}()
err = ioutil.WriteFile(tempFileName, bs, 0600)
if err != nil {
return err
}
err = os.Rename(tempFileName, file)
if err != nil {
return err
}