Compare commits

...

21 Commits

Author SHA1 Message Date
shatoboar
c86c620016 Fix(auth0) caching Users by accountId 2022-06-03 17:19:31 +02:00
shatoboar
1e444f58c1 Merge remote-tracking branch 'origin' into users_cache 2022-06-03 14:42:06 +02:00
shatoboar
f53990d6c1 WIP idpmanager users_cache by accountId 2022-06-03 14:39:07 +02:00
Misha Bragin
02a6ac44be Handle Network out of range (#347) 2022-06-03 14:39:07 +02:00
Misha Bragin
43e472c958 Update links in Start using NetBird (#346)
* Update links in Start using NetBird

* Update internals overview and co structure

* Netbird to NetBird
2022-06-03 14:39:07 +02:00
Misha Bragin
60ac8c3268 Handle Network out of range (#347) 2022-06-02 12:56:02 +02:00
shatoboar
cea5693512 Feat(auth0.go) Cache for users in idpmanager 2022-06-01 21:52:16 +02:00
shatoboar
49ec33504a Implemented caching logic for auth0 2022-05-31 17:29:51 +02:00
Misha Bragin
2e5d4ba6fa Update links in Start using NetBird (#346)
* Update links in Start using NetBird

* Update internals overview and co structure

* Netbird to NetBird
2022-05-31 16:06:34 +02:00
Misha Bragin
0fbe78375e Log whether kernel or userspace WireGuard is used (#345) 2022-05-30 15:52:43 +02:00
Misha Bragin
87631cbc8b Replace IP allocation logic (#342)
The peer IP allocation logic was allocating sequential peer IP from the 100.64.0.0/10 
address block.
Each account is created with a random subnet from 100.64.0.0/10.
The total amount of potential subnets is 64.
The new logic allocates random peer IP
from the account subnet.
This gives us flexibility to add support for
multi subnet accounts without overlapping IPs.
2022-05-29 22:43:39 +02:00
Misha Bragin
ec39202590 Referer README installation steps to docs website (#344) 2022-05-29 22:39:33 +02:00
Maycon Santos
b227a7c34e Add NETBIRD_MGMT_GRPC_API_ENDPOINT support to our scripts (#341) 2022-05-28 20:47:44 +02:00
Maycon Santos
c86bacb5c3 Unblock menu when login (#340)
* GetClientID method and increase interval on slow_down err

* Reuse existing authentication flow if is not expired

Created a new struct to hold additional info
 about the flow

 If there is a waiting sso running, we cancel its context

* Run the up command on a goroutine

* Use time.Until

* Use proper ctx and consistently use goroutine for up/down
2022-05-28 18:37:08 +02:00
Misha Bragin
59a964eed8 Change network mask to limit number of peers to 65k (#339) 2022-05-28 12:54:09 +02:00
Misha Bragin
feff6dc966 Update announcement bar in README 2022-05-28 09:48:51 +02:00
Maycon Santos
258cb3d43b Fix UP calls when state is idle (#338)
* Fix UP calls when state is idle

When we want to login we can call server.Login
It already checks the login status of the peer

* Remove unused status

* Defer close daemon client conn

Co-authored-by: braginini <bangvalo@gmail.com>
2022-05-27 19:16:58 +02:00
Misha Bragin
4088aaf6fe Pass engine context to management and signal clients (#337) 2022-05-27 15:54:51 +02:00
Misha Bragin
1bb504ea78 Fix peer status Connected when removed from the management (#336) 2022-05-27 15:26:36 +02:00
Maycon Santos
594da0a6b8 Display client's version on UI (#335) 2022-05-27 13:56:12 +02:00
Misha Bragin
889fa646fc Fix duplicate output of interactive login (#334) 2022-05-27 13:55:24 +02:00
27 changed files with 606 additions and 410 deletions

219
README.md
View File

@@ -1,6 +1,6 @@
<p align="center">
<strong>Big News! Wiretrustee becomes Netbird</strong>.
<a href="https://netbird.io/blog/wiretrustee-becomes-netbird">
<strong>:hatching_chick: New release! Beta Update May 2022</strong>.
<a href="https://github.com/netbirdio/netbird/releases/tag/v0.6.0">
Learn more
</a>
</p>
@@ -18,8 +18,7 @@
</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" />
</a>
<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">
@@ -35,7 +34,7 @@
<p align="center">
<strong>
Start using Netbird at <a href="https://app.netbird.io/">app.netbird.io</a>
Start using NetBird at <a href="https://app.netbird.io/">app.netbird.io</a>
<br/>
See <a href="https://netbird.io/docs/">Documentation</a>
<br/>
@@ -47,15 +46,15 @@
<br>
**Netbird is an open-source VPN management platform built on top of WireGuard® making it easy to create secure private networks for your organization or home.**
**NetBird is an open-source VPN management platform built on top of WireGuard® making it easy to create secure private networks for your organization or home.**
It requires zero configuration effort leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
Netbird creates an overlay peer-to-peer network connecting machines automatically regardless of their location (home, office, datacenter, container, cloud or edge environments) unifying virtual private network management experience.
NetBird creates an overlay peer-to-peer network connecting machines automatically regardless of their location (home, office, datacenter, container, cloud or edge environments) unifying virtual private network management experience.
**Key features:**
* Automatic IP allocation and management.
* Automatic peer (machine) discovery and configuration.
* Automatic WireGuard peer (machine) discovery and configuration.
* Encrypted peer-to-peer connections without a central VPN gateway.
* Connection relay fallback in case a peer-to-peer connection is not possible.
* Network management layer with a neat Web UI panel ([separate repo](https://github.com/netbirdio/dashboard))
@@ -78,23 +77,26 @@ Netbird creates an overlay peer-to-peer network connecting machines automaticall
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
Hosted version: [https://app.netbird.io/](https://app.netbird.io/)
[Web UI repository](https://github.com/netbirdio/dashboard)
### Start using NetBird
* Hosted version: [https://app.netbird.io/](https://app.netbird.io/).
* See our documentation for [Quickstart Guide](https://netbird.io/docs/getting-started/quickstart).
* If you are looking to self-host NetBird, check our [Self-Hosting Guide](https://netbird.io/docs/getting-started/self-hosting).
* Step-by-step [Installation Guide](https://netbird.io/docs/getting-started/installation) for different platforms.
* Web UI [repository](https://github.com/netbirdio/dashboard).
* 5 min [demo video](https://youtu.be/Tu9tPsUWaY0) on YouTube.
### A bit on Netbird internals
* Every machine in the network runs [Netbird Agent (or Client)](client/) that manages WireGuard.
* Netbird features a [Management Service](management/) that offers peer IP management and network updates distribution (e.g. when a new machine joins the network others are getting notified if allowed by access controls). Simply put, this service holds the state of the network.
### A bit on NetBird internals
* Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
* NetBird features [Management Service](management/) that holds network state, manages peer IPs, and distributes network updates to peers.
* Every agent is connected to Management Service.
* Netbird agent uses WebRTC ICE implemented in [pion/ice library](https://github.com/pion/ice) to discover connection candidates when establishing a peer-to-peer connection between machines.
* NetBird agent uses WebRTC ICE implemented in [pion/ice library](https://github.com/pion/ice) to discover connection candidates when establishing a peer-to-peer connection between machines.
* Connection candidates are discovered with a help of [STUN](https://en.wikipedia.org/wiki/STUN) server.
* Agents negotiate a connection through [Signal Service](signal/).
* Signal Service uses public Wireguard keys to route messages between peers.
Contents of the messages sent between peers through the signaling server are encrypted with Wireguard keys, making it impossible to inspect them.
* Sometimes the NAT traversal is unsuccessful due to strict NATs (e.g. mobile carrier-grade NAT) and p2p connection isn't possible. When this occurs the system falls back to a relay server called [TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT), and a secure Wireguard tunnel is established via the TURN server.
* Agents negotiate a connection through [Signal Service](signal/) passing p2p encrypted messages.
* Signal Service uses public WireGuard keys to route messages between peers.
* Sometimes the NAT traversal is unsuccessful due to strict NATs (e.g. mobile carrier-grade NAT) and p2p connection isn't possible. When this occurs the system falls back to a relay server called [TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT), and a secure WireGuard tunnel is established via the TURN server.
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in Netbird setups.
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in NetBird setups.
<p float="left" align="middle">
<img src="https://netbird.io/docs/img/architecture/high-level-dia.png" width="700"/>
@@ -102,182 +104,11 @@ Hosted version: [https://app.netbird.io/](https://app.netbird.io/)
See a complete [architecture overview](https://netbird.io/docs/overview/architecture) for details.
**Testimonials:** We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), and [Coturn](https://github.com/coturn/coturn). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
### Product Roadmap
### Roadmap
- [Public Roadmap](https://github.com/netbirdio/netbird/projects/2)
### Client Installation
#### Linux
**APT/Debian**
1. Add the repository:
```shell
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg -y
curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo apt-key add -
echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list
```
2. Update APT's cache
```shell
sudo apt-get update
```
3. Install the package
```shell
# for CLI only
sudo apt-get install netbird
# for GUI package
sudo apt-get install netbird-ui
```
**RPM/Red hat**
1. Add the repository:
```shell
cat <<EOF | sudo tee /etc/yum.repos.d/wiretrustee.repo
[Wiretrustee]
name=Wiretrustee
baseurl=https://pkgs.wiretrustee.com/yum/
enabled=1
gpgcheck=0
gpgkey=https://pkgs.wiretrustee.com/yum/repodata/repomd.xml.key
repo_gpgcheck=1
EOF
```
2. Install the package
```shell
# for CLI only
sudo yum install netbird
# for GUI package
sudo yum install netbird-ui
```
#### MACOS
**Homebrew install**
1. Download and install homebrew at https://brew.sh/
2. If wiretrustee was previously installed with homebrew, you will need to run:
```shell
# Stop and uninstall daemon service:
sudo wiretrustee service stop
sudo wiretrustee service uninstall
# unlik the app
brew unlink wiretrustee
```
> netbird will copy any existing configuration from the Wiretrustee's default configuration paths to the new Netbird's default location
3. Install the client
```shell
# for CLI only
brew install netbirdio/tap/netbird
# for GUI package
brew install --cask netbirdio/tap/netbird-ui
```
4. If you are install CLI only, you need to install and start the client daemon service:
```shell
sudo netbird service install
sudo netbird service start
```
**Installation from binary (CLI only)**
1. Checkout Netbird [releases](https://github.com/netbirdio/netbird/releases/latest)
2. Download the latest release (**Switch VERSION to the latest**):
```shell
curl -o ./netbird_<VERSION>_darwin_amd64.tar.gz https://github.com/netbirdio/netbird/releases/download/v<VERSION>/wiretrustee_<VERSION>_darwin_amd64.tar.gz
```
3. Decompress
```shell
tar xcf ./netbird_<VERSION>_darwin_amd64.tar.gz
sudo mv netbird /usr/bin/netbird
chmod +x /usr/bin/netbird
```
After that you may need to add /usr/bin in your PATH environment variable:
````shell
export PATH=$PATH:/usr/bin
````
4. Install and run the service
```shell
sudo netbird service install
sudo netbird service start
```
#### Windows
1. Checkout Netbird [releases](https://github.com/netbirdio/netbird/releases/latest)
2. Download the latest Windows release installer ```netbird_installer_<VERSION>_windows_amd64.exe``` (**Switch VERSION to the latest**):
3. Proceed with installation steps
4. This will install the client in the C:\\Program Files\\Netbird and add the client service
5. After installing, you can follow the [Client Configuration](#Client-Configuration) steps.
> To uninstall the client and service, you can use Add/Remove programs
### Client Configuration
If you installed the UI client, you can launch it and click on Connect
> It will open your browser, and you will be prompt for email and password
Simply run:
```shell
netbird up
```
> It will open your browser, and you will be prompt for email and password
Check connection status:
```shell
netbird status
```
In case you are activating a server peer, you can use a setup-key as described in the steps below:
1. Login to the Management Service. You need to have a `setup key` in hand (see ).
For all systems:
```shell
netbird up --setup-key <SETUP KEY>
```
For **Docker**, you can run with the following command:
```shell
docker run --network host --privileged --rm -d -e NB_SETUP_KEY=<SETUP KEY> -v netbird-client:/etc/netbird netbirdio/netbird:<TAG>
```
> TAG > 0.6.0 version
Alternatively, if you are hosting your own Management Service provide `--management-url` property pointing to your Management Service:
```shell
sudo netbird up --setup-key <SETUP KEY> --management-url http://localhost:33073
```
> You could also omit the `--setup-key` property. In this case, the tool will prompt for the key.
2. Check connection status:
```shell
netbird status
```
3. Check your IP:
For **MACOS** you will just start the service:
````shell
sudo ifconfig utun100
````
For **Linux** systems:
```shell
ip addr show wt0
```
For **Windows** systems:
```shell
netsh interface ip show config name="wt0"
```
4. Repeat on other machines.
### Troubleshooting
1. If you are using self-hosted version and haven't specified `--management-url`, the client app will use the default URL
which is ```https://api.wiretrustee.com:33073```.
2. If you have specified a wrong `--management-url` (e.g., just by mistake when self-hosting)
to override it you can do the following:
```shell
netbird down
netbird up --management-url https://<CORRECT HOST:PORT>/
```
To override it see solution #1 above.
### Running Dashboard, Management, Signal and Coturn
See [Self-Hosting Guide](https://netbird.io/docs/getting-started/self-hosting)
### Testimonials
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), and [Coturn](https://github.com/coturn/coturn). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
### Legal
[WireGuard](https://wireguard.com/) is a registered trademark of Jason A. Donenfeld.

View File

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

View File

@@ -92,7 +92,7 @@ var loginCmd = &cobra.Command{
}
if loginResp.NeedsSSOLogin {
openURL(cmd, loginResp.VerificationURI, loginResp.VerificationURIComplete, loginResp.UserCode)
openURL(cmd, loginResp.VerificationURIComplete)
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
if err != nil {
@@ -175,7 +175,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
return nil, fmt.Errorf("getting a request device code failed: %v", err)
}
openURL(cmd, flowInfo.VerificationURI, flowInfo.VerificationURIComplete, flowInfo.UserCode)
openURL(cmd, flowInfo.VerificationURIComplete)
waitTimeout := time.Duration(flowInfo.ExpiresIn)
waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout*time.Second)
@@ -189,13 +189,12 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
return &tokenInfo, nil
}
func openURL(cmd *cobra.Command, verificationURI, verificationURIComplete, userCode string) {
func openURL(cmd *cobra.Command, verificationURIComplete string) {
err := open.Run(verificationURIComplete)
cmd.Printf("Please do the SSO login in your browser. \n" +
"If your browser didn't open automatically, use this URL to log in:\n\n" +
" " + verificationURIComplete + " \n\n")
if err != nil {
cmd.Println("Unable to open the default browser.")
cmd.Println("If this is not an interactive shell, you may want to use the setup key, see https://www.netbird.io/docs/overview/setup-keys")
cmd.Printf("Otherwise, you can continue the login flow by accessing the url below:\n\t%s\n", verificationURI)
cmd.Printf("Use the access code: %s\n", userCode)
cmd.Println("Or press CTRL + C or COMMAND + C")
cmd.Printf("Alternatively, you may want to use a setup key, see:\n\n https://www.netbird.io/docs/overview/setup-keys\n")
}
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/util"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
@@ -55,7 +56,13 @@ var upCmd = &cobra.Command{
"If the daemon is not running please run: "+
"\nnetbird service install \nnetbird service start\n", err)
}
defer conn.Close()
defer func() {
err := conn.Close()
if err != nil {
log.Warnf("failed closing dameon gRPC client connection %v", err)
return
}
}()
client := proto.NewDaemonServiceClient(conn)
@@ -64,55 +71,51 @@ var upCmd = &cobra.Command{
return fmt.Errorf("unable to get daemon status: %v", err)
}
if status.Status == string(internal.StatusNeedsLogin) || status.Status == string(internal.StatusLoginFailed) {
loginRequest := proto.LoginRequest{
SetupKey: setupKey,
PreSharedKey: preSharedKey,
ManagementUrl: managementURL,
}
var loginErr error
var loginResp *proto.LoginResponse
err = WithBackOff(func() error {
var backOffErr error
loginResp, backOffErr = client.Login(ctx, &loginRequest)
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
s.Code() == codes.PermissionDenied ||
s.Code() == codes.NotFound ||
s.Code() == codes.Unimplemented) {
loginErr = backOffErr
return nil
}
return backOffErr
})
if err != nil {
return fmt.Errorf("login backoff cycle failed: %v", err)
}
if loginErr != nil {
return fmt.Errorf("login failed: %v", loginErr)
}
if loginResp.NeedsSSOLogin {
openURL(cmd, loginResp.VerificationURI, loginResp.VerificationURIComplete, loginResp.UserCode)
cmd.Printf("Please do the SSO login in your browser. \n" +
"If your browser didn't open automatically, use this URL to log in:\n\n" +
" " + loginResp.VerificationURIComplete + " \n\n")
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
if err != nil {
return fmt.Errorf("waiting sso login failed with: %v", err)
}
}
} else if status.Status != string(internal.StatusIdle) {
if status.Status == string(internal.StatusConnected) {
cmd.Println("Already connected")
return nil
}
loginRequest := proto.LoginRequest{
SetupKey: setupKey,
PreSharedKey: preSharedKey,
ManagementUrl: managementURL,
}
var loginErr error
var loginResp *proto.LoginResponse
err = WithBackOff(func() error {
var backOffErr error
loginResp, backOffErr = client.Login(ctx, &loginRequest)
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
s.Code() == codes.PermissionDenied ||
s.Code() == codes.NotFound ||
s.Code() == codes.Unimplemented) {
loginErr = backOffErr
return nil
}
return backOffErr
})
if err != nil {
return fmt.Errorf("login backoff cycle failed: %v", err)
}
if loginErr != nil {
return fmt.Errorf("login failed: %v", loginErr)
}
if loginResp.NeedsSSOLogin {
openURL(cmd, loginResp.VerificationURIComplete)
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
if err != nil {
return fmt.Errorf("waiting sso login failed with: %v", err)
}
}
if _, err := client.Up(ctx, &proto.UpRequest{}); err != nil {
return fmt.Errorf("call service up method: %v", err)
}

View File

@@ -60,8 +60,11 @@ func RunClient(ctx context.Context, config *Config) error {
mgmTlsEnabled = true
}
engineCtx, cancel := context.WithCancel(ctx)
defer cancel()
// 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)
mgmClient, loginResp, err := connectToManagement(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
if err != nil {
log.Debug(err)
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
@@ -73,7 +76,7 @@ func RunClient(ctx context.Context, config *Config) error {
}
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
signalClient, err := connectToSignal(ctx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
if err != nil {
log.Error(err)
return wrapErr(err)
@@ -87,9 +90,6 @@ func RunClient(ctx context.Context, config *Config) error {
return wrapErr(err)
}
engineCtx, cancel := context.WithCancel(ctx)
defer cancel()
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig)
err = engine.Start()
if err != nil {

View File

@@ -16,6 +16,7 @@ type OAuthClient interface {
RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
RotateAccessToken(ctx context.Context, refreshToken string) (TokenInfo, error)
WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error)
GetClientID(ctx context.Context) string
}
// HTTPClient http client interface for API calls
@@ -104,6 +105,11 @@ func NewHostedDeviceFlow(audience string, clientID string, domain string) *Hoste
}
}
// GetClientID returns the provider client id
func (h *Hosted) GetClientID(ctx context.Context) string {
return h.ClientID
}
// RequestDeviceCode requests a device code login flow information from Hosted
func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error) {
url := "https://" + h.Domain + "/oauth/device/code"
@@ -150,7 +156,8 @@ func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
// WaitToken waits user's login and authorize the app. Once the user's authorize
// it retrieves the access token from Hosted's endpoint and validates it before returning
func (h *Hosted) WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error) {
ticker := time.NewTicker(time.Duration(info.Interval) * time.Second)
interval := time.Duration(info.Interval) * time.Second
ticker := time.NewTicker(interval)
for {
select {
case <-ctx.Done():
@@ -181,7 +188,12 @@ func (h *Hosted) WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo,
if tokenResponse.Error != "" {
if tokenResponse.Error == "authorization_pending" {
continue
} else if tokenResponse.Error == "slow_down" {
interval = interval + (3 * time.Second)
ticker.Reset(interval)
continue
}
return TokenInfo{}, fmt.Errorf(tokenResponse.ErrorDescription)
}

View File

@@ -26,14 +26,20 @@ type Server struct {
configPath string
logFile string
oauthClient internal.OAuthClient
deviceAuthInfo internal.DeviceAuthInfo
oauthAuthFlow oauthAuthFlow
mutex sync.Mutex
config *internal.Config
proto.UnimplementedDaemonServiceServer
}
type oauthAuthFlow struct {
expiresAt time.Time
client internal.OAuthClient
info internal.DeviceAuthInfo
waitCancel context.CancelFunc
}
// New server instance constructor.
func New(ctx context.Context, managementURL, adminURL, configPath, logFile string) *Server {
return &Server{
@@ -187,6 +193,21 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
providerConfig.ProviderConfig.Domain,
)
if s.oauthAuthFlow.client != nil && s.oauthAuthFlow.client.GetClientID(ctx) == hostedClient.GetClientID(context.TODO()) {
if s.oauthAuthFlow.expiresAt.After(time.Now().Add(90 * time.Second)) {
log.Debugf("using previous device flow info")
return &proto.LoginResponse{
NeedsSSOLogin: true,
VerificationURI: s.oauthAuthFlow.info.VerificationURI,
VerificationURIComplete: s.oauthAuthFlow.info.VerificationURIComplete,
UserCode: s.oauthAuthFlow.info.UserCode,
}, nil
} else {
log.Warnf("canceling previous waiting execution")
s.oauthAuthFlow.waitCancel()
}
}
deviceAuthInfo, err := hostedClient.RequestDeviceCode(context.TODO())
if err != nil {
log.Errorf("getting a request device code failed: %v", err)
@@ -194,8 +215,9 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
}
s.mutex.Lock()
s.oauthClient = hostedClient
s.deviceAuthInfo = deviceAuthInfo
s.oauthAuthFlow.client = hostedClient
s.oauthAuthFlow.info = deviceAuthInfo
s.oauthAuthFlow.expiresAt = time.Now().Add(time.Duration(deviceAuthInfo.ExpiresIn) * time.Second)
s.mutex.Unlock()
state.Set(internal.StatusNeedsLogin)
@@ -233,7 +255,7 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
s.actCancel = cancel
s.mutex.Unlock()
if s.oauthClient == nil {
if s.oauthAuthFlow.client == nil {
return nil, gstatus.Errorf(codes.Internal, "oauth client is not initialized")
}
@@ -248,7 +270,7 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
state.Set(internal.StatusConnecting)
s.mutex.Lock()
deviceAuthInfo := s.deviceAuthInfo
deviceAuthInfo := s.oauthAuthFlow.info
s.mutex.Unlock()
if deviceAuthInfo.UserCode != msg.UserCode {
@@ -256,12 +278,26 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
return nil, gstatus.Errorf(codes.InvalidArgument, "sso user code is invalid")
}
waitTimeout := time.Duration(deviceAuthInfo.ExpiresIn)
waitCTX, cancel := context.WithTimeout(ctx, waitTimeout*time.Second)
if s.oauthAuthFlow.waitCancel != nil {
s.oauthAuthFlow.waitCancel()
}
waitTimeout := time.Until(s.oauthAuthFlow.expiresAt)
waitCTX, cancel := context.WithTimeout(ctx, waitTimeout)
defer cancel()
tokenInfo, err := s.oauthClient.WaitToken(waitCTX, deviceAuthInfo)
s.mutex.Lock()
s.oauthAuthFlow.waitCancel = cancel
s.mutex.Unlock()
tokenInfo, err := s.oauthAuthFlow.client.WaitToken(waitCTX, deviceAuthInfo)
if err != nil {
if err == context.Canceled {
return nil, nil
}
s.mutex.Lock()
s.oauthAuthFlow.expiresAt = time.Now()
s.mutex.Unlock()
state.Set(internal.StatusLoginFailed)
log.Errorf("waiting for browser login failed: %v", err)
return nil, err

View File

@@ -18,7 +18,7 @@
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
"Net": {
"IP": "100.64.0.0",
"Mask": "/8AAAA=="
"Mask": "//8AAA=="
},
"Dns": null
},

View File

@@ -90,7 +90,7 @@ type serviceClient struct {
icConnected []byte
icDisconnected []byte
// systray menu itmes
// systray menu items
mStatus *systray.MenuItem
mUp *systray.MenuItem
mDown *systray.MenuItem
@@ -259,36 +259,27 @@ func (s *serviceClient) menuUpClick() error {
return err
}
err = s.login()
if err != nil {
log.Errorf("login failed with: %v", err)
return err
}
status, err := conn.Status(s.ctx, &proto.StatusRequest{})
if err != nil {
log.Errorf("get service status: %v", err)
return err
}
if status.Status == string(internal.StatusNeedsLogin) || status.Status == string(internal.StatusLoginFailed) {
err = s.login()
if err != nil {
log.Errorf("get service status: %v", err)
return err
}
}
status, err = conn.Status(s.ctx, &proto.StatusRequest{})
if err != nil {
log.Errorf("get service status: %v", err)
return err
}
if status.Status != string(internal.StatusIdle) {
if status.Status == string(internal.StatusConnected) {
log.Warnf("already connected")
return nil
return err
}
if _, err := s.conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
log.Errorf("up service: %v", err)
return err
}
return nil
}
@@ -373,6 +364,9 @@ func (s *serviceClient) onTrayReady() {
systray.AddSeparator()
s.mSettings = systray.AddMenuItem("Settings", "Settings of the application")
systray.AddSeparator()
v := systray.AddMenuItem("v"+system.NetbirdVersion(), "Client Version: "+system.NetbirdVersion())
v.Disable()
systray.AddSeparator()
s.mQuit = systray.AddMenuItem("Quit", "Quit the client app")
go func() {
@@ -393,15 +387,19 @@ func (s *serviceClient) onTrayReady() {
case <-s.mAdminPanel.ClickedCh:
err = open.Run(s.adminURL)
case <-s.mUp.ClickedCh:
s.mUp.Disable()
if err = s.menuUpClick(); err != nil {
s.mUp.Enable()
}
go func() {
err := s.menuUpClick()
if err != nil {
return
}
}()
case <-s.mDown.ClickedCh:
s.mDown.Disable()
if err = s.menuDownClick(); err != nil {
s.mDown.Enable()
}
go func() {
err := s.menuDownClick()
if err != nil {
return
}
}()
case <-s.mSettings.ClickedCh:
s.mSettings.Disable()
go func() {

1
go.mod
View File

@@ -29,6 +29,7 @@ require (
require (
fyne.io/fyne/v2 v2.1.4
github.com/c-robinson/iplib v1.0.3
github.com/getlantern/systray v1.2.1
github.com/magiconair/properties v1.8.5
github.com/rs/xid v1.3.0

2
go.sum
View File

@@ -70,6 +70,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/c-robinson/iplib v1.0.3 h1:NG0UF0GoEsrC1/vyfX1Lx2Ss7CySWl3KqqXh3q4DdPU=
github.com/c-robinson/iplib v1.0.3/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=

View File

@@ -38,9 +38,10 @@ func WireguardModExists() bool {
func (w *WGIface) Create() error {
if WireguardModExists() {
log.Debug("using kernel Wireguard module")
log.Info("using kernel WireGuard")
return w.CreateWithKernel()
} else {
log.Info("using userspace WireGuard")
return w.CreateWithUserspace()
}
}

View File

@@ -12,6 +12,7 @@ fi
if [[ $NETBIRD_DOMAIN == "localhost" || $NETBIRD_DOMAIN == "127.0.0.1" ]]
then
export NETBIRD_MGMT_API_ENDPOINT=http://$NETBIRD_DOMAIN:$NETBIRD_MGMT_API_PORT
export NETBIRD_MGMT_GRPC_API_ENDPOINT=http://$NETBIRD_DOMAIN:$NETBIRD_MGMT_GRPC_API_PORT
unset NETBIRD_MGMT_API_CERT_FILE
unset NETBIRD_MGMT_API_CERT_KEY_FILE
fi

View File

@@ -12,6 +12,7 @@ services:
- AUTH0_CLIENT_ID=$NETBIRD_AUTH0_CLIENT_ID
- AUTH0_AUDIENCE=$NETBIRD_AUTH0_AUDIENCE
- NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
- NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_MGMT_GRPC_API_ENDPOINT
- NGINX_SSL_PORT=443
- LETSENCRYPT_DOMAIN=$NETBIRD_DOMAIN
- LETSENCRYPT_EMAIL=$NETBIRD_LETSENCRYPT_EMAIL
@@ -39,7 +40,7 @@ services:
- $LETSENCRYPT_VOLUMENAME:/etc/letsencrypt:ro
- ./management.json:/etc/netbird/management.json
ports:
- 33073:33073 #gRPC port
- $NETBIRD_MGMT_GRPC_API_PORT:33073 #gRPC port
- $NETBIRD_MGMT_API_PORT:33071 #API port
# # port and command for Let's Encrypt validation
# - 443:443

View File

@@ -19,8 +19,12 @@ NETBIRD_LETSENCRYPT_EMAIL=""
# Management API port
NETBIRD_MGMT_API_PORT=33071
# Management GRPC API port
NETBIRD_MGMT_GRPC_API_PORT=33073
# Management API endpoint address, used by the Dashboard
NETBIRD_MGMT_API_ENDPOINT=https://$NETBIRD_DOMAIN:$NETBIRD_MGMT_API_PORT
# Management GRPC API endpoint address, used by the hosts to register
NETBIRD_MGMT_GRPC_API_ENDPOINT=https://$NETBIRD_DOMAIN:NETBIRD_MGMT_GRPC_API_PORT
# Management Certficate file path. These are generated by the Dashboard container
NETBIRD_MGMT_API_CERT_FILE="/etc/letsencrypt/live/$NETBIRD_DOMAIN/fullchain.pem"
# Management Certficate key file path.
@@ -50,6 +54,8 @@ export NETBIRD_AUTH0_AUDIENCE
export NETBIRD_LETSENCRYPT_EMAIL
export NETBIRD_MGMT_API_PORT
export NETBIRD_MGMT_API_ENDPOINT
export NETBIRD_MGMT_GRPC_API_PORT
export NETBIRD_MGMT_GRPC_API_ENDPOINT
export NETBIRD_MGMT_API_CERT_FILE
export NETBIRD_MGMT_API_CERT_KEY_FILE
export TURN_USER

View File

@@ -4,6 +4,8 @@ import (
"context"
"crypto/tls"
"fmt"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"io"
"time"
@@ -115,6 +117,9 @@ func (c *GrpcClient) Sync(msgHandler func(msg *proto.SyncResponse) error) error
// blocking until error
err = c.receiveEvents(stream, *serverPubKey, msgHandler)
if err != nil {
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
return backoff.Permanent(err)
}
backOff.Reset()
return err
}
@@ -124,7 +129,7 @@ func (c *GrpcClient) Sync(msgHandler func(msg *proto.SyncResponse) error) error
err := backoff.Retry(operation, backOff)
if err != nil {
log.Warnf("exiting Management Service connection retry loop due to unrecoverable error: %s", err)
log.Warnf("exiting Management Service connection retry loop due to Permanent error: %s", err)
return err
}

View File

@@ -328,11 +328,12 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID string) ([]*UserI
queriedUsers := make([]*idp.UserData, 0)
if !isNil(am.idpManager) {
queriedUsers, err = am.idpManager.GetBatchedUserData(accountID)
queriedUsers, err = am.idpManager.GetAllUsers(accountID)
if err != nil {
return nil, err
}
}
// TODO: we need to check whether we need to refresh our cache or not
userInfo := make([]*UserInfo, 0)
@@ -352,7 +353,6 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID string) ([]*UserI
for _, queriedUser := range queriedUsers {
if localUser, contains := account.Users[queriedUser.ID]; contains {
userInfo = append(userInfo, mergeLocalAndQueryUser(*queriedUser, *localUser))
log.Debugf("Merged userinfo to send back; %v", userInfo)
}
}

View File

@@ -265,10 +265,6 @@ func TestAccountManager_AddAccount(t *testing.T) {
userId := "account_creator"
expectedPeersSize := 0
expectedSetupKeysSize := 2
expectedNetwork := net.IPNet{
IP: net.IP{100, 64, 0, 0},
Mask: net.IPMask{255, 192, 0, 0},
}
account, err := manager.AddAccount(expectedId, userId, "")
if err != nil {
@@ -287,8 +283,9 @@ func TestAccountManager_AddAccount(t *testing.T) {
t.Errorf("expected account to have len(SetupKeys) = %v, got %v", expectedSetupKeysSize, len(account.SetupKeys))
}
if account.Network.Net.String() != expectedNetwork.String() {
t.Errorf("expected account to have Network = %v, got %v", expectedNetwork.String(), account.Network.Net.String())
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
if !ipNet.Contains(account.Network.Net.IP) {
t.Errorf("expected account's Network to be a subnet of %v, got %v", ipNet.String(), account.Network.Net.String())
}
}
@@ -419,7 +416,6 @@ func TestAccountManager_AddPeer(t *testing.T) {
return
}
expectedPeerKey := key.PublicKey().String()
expectedPeerIP := "100.64.0.1"
expectedSetupKey := setupKey.Key
peer, err := manager.AddPeer(setupKey.Key, "", &Peer{
@@ -442,8 +438,8 @@ func TestAccountManager_AddPeer(t *testing.T) {
t.Errorf("expecting just added peer to have key = %s, got %s", expectedPeerKey, peer.Key)
}
if peer.IP.String() != expectedPeerIP {
t.Errorf("expecting just added peer to have IP = %s, got %s", expectedPeerIP, peer.IP.String())
if !account.Network.Net.Contains(peer.IP) {
t.Errorf("expecting just added peer's IP %s to be in a network range %s", peer.IP.String(), account.Network.Net.String())
}
if peer.SetupKey != expectedSetupKey {
@@ -482,7 +478,6 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) {
return
}
expectedPeerKey := key.PublicKey().String()
expectedPeerIP := "100.64.0.1"
expectedUserId := userId
peer, err := manager.AddPeer("", userId, &Peer{
@@ -505,8 +500,8 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) {
t.Errorf("expecting just added peer to have key = %s, got %s", expectedPeerKey, peer.Key)
}
if peer.IP.String() != expectedPeerIP {
t.Errorf("expecting just added peer to have IP = %s, got %s", expectedPeerIP, peer.IP.String())
if !account.Network.Net.Contains(peer.IP) {
t.Errorf("expecting just added peer's IP %s to be in a network range %s", peer.IP.String(), account.Network.Net.String())
}
if peer.UserID != expectedUserId {
@@ -596,7 +591,7 @@ func TestGetUsersFromAccount(t *testing.T) {
for _, userInfo := range userInfos {
id := userInfo.ID
assert.Equal(t, userInfo.ID, users[id].Id)
assert.Equal(t, string(userInfo.Role), string(users[id].Role))
assert.Equal(t, userInfo.Role, string(users[id].Role))
assert.Equal(t, userInfo.Name, "")
assert.Equal(t, userInfo.Email, "")
}

View File

@@ -203,8 +203,11 @@ func (s *Server) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest) (*Pe
},
})
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.FailedPrecondition {
return nil, err
s, ok := status.FromError(err)
if ok {
if s.Code() == codes.FailedPrecondition || s.Code() == codes.OutOfRange {
return nil, err
}
}
return nil, status.Errorf(codes.NotFound, "provided setup key doesn't exists")
}
@@ -365,7 +368,7 @@ func toWiretrusteeConfig(config *Config, turnCredentials *TURNCredentials) *prot
func toPeerConfig(peer *Peer) *proto.PeerConfig {
return &proto.PeerConfig{
Address: peer.IP.String() + "/24", // todo make it explicit
Address: peer.IP.String() + "/16", // todo make it explicit
}
}

View File

@@ -1,6 +1,9 @@
package idp
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
@@ -18,10 +21,11 @@ import (
// Auth0Manager auth0 manager client instance
type Auth0Manager struct {
authIssuer string
httpClient ManagerHTTPClient
credentials ManagerCredentials
helper ManagerHelper
authIssuer string
httpClient ManagerHTTPClient
credentials ManagerCredentials
helper ManagerHelper
cachedUsersByAccountId map[string][]Auth0Profile
}
// Auth0ClientConfig auth0 manager client configurations
@@ -51,6 +55,38 @@ type Auth0Credentials struct {
mux sync.Mutex
}
type Auth0Profile struct {
AccountId string `json:"wt_account_id"`
UserID string `json:"user_id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt string `json:"created_at"`
LastLogin string `json:"last_login"`
}
type UserExportJobResponse struct {
Type string `json:"type"`
Status string `json:"status"`
ConnectionId string `json:"connection_id"`
Format string `json:"format"`
Limit int `json:"limit"`
Connection string `json:"connection"`
CreatedAt time.Time `json:"created_at"`
Id string `json:"id"`
}
type ExportJobStatusResponse struct {
Type string `json:"type"`
Status string `json:"status"`
ConnectionId string `json:"connection_id"`
Format string `json:"format"`
Limit int `json:"limit"`
Location string `json:"location"`
Connection string `json:"connection"`
CreatedAt time.Time `json:"created_at"`
Id string `json:"id"`
}
// NewAuth0Manager creates a new instance of the Auth0Manager
func NewAuth0Manager(config Auth0ClientConfig) (*Auth0Manager, error) {
@@ -81,11 +117,13 @@ func NewAuth0Manager(config Auth0ClientConfig) (*Auth0Manager, error) {
httpClient: httpClient,
helper: helper,
}
return &Auth0Manager{
authIssuer: config.AuthIssuer,
credentials: credentials,
httpClient: httpClient,
helper: helper,
authIssuer: config.AuthIssuer,
credentials: credentials,
httpClient: httpClient,
helper: helper,
cachedUsersByAccountId: make(map[string][]Auth0Profile),
}, nil
}
@@ -186,44 +224,198 @@ func (c *Auth0Credentials) Authenticate() (JWTToken, error) {
return c.jwtToken, nil
}
func batchRequestUsersUrl(authIssuer, accountId string, page int) (string, url.Values, error) {
u, err := url.Parse(authIssuer + "/api/v2/users")
if err != nil {
return "", nil, err
}
q := u.Query()
q.Set("page", strconv.Itoa(page))
q.Set("search_engine", "v3")
q.Set("q", "app_metadata.wt_account_id:"+accountId)
u.RawQuery = q.Encode()
return u.String(), q, nil
}
func requestByUserIdUrl(authIssuer, userId string) string {
return authIssuer + "/api/v2/users/" + userId
}
// GetBatchedUserData requests users in batches from Auth0
func (am *Auth0Manager) GetBatchedUserData(accountId string) ([]*UserData, error) {
jwtToken, err := am.credentials.Authenticate()
if err != nil {
return nil, err
// Gets all users from cache, if the cache exists
// Otherwise we will initialize the cache with creating the export job on auth0
func (am *Auth0Manager) GetAllUsers(accountId string) ([]*UserData, error) {
if len(am.cachedUsersByAccountId[accountId]) == 0 {
err := am.createExportUsersJob(accountId)
if err != nil {
log.Debugf("Couldn't cache users; %v", err)
return nil, err
}
}
var list []*UserData
cachedUsers := am.cachedUsersByAccountId[accountId]
for _, val := range cachedUsers {
list = append(list, &UserData{
Name: val.Name,
Email: val.Email,
ID: val.UserID,
})
}
return list, nil
}
// This creates an export job on auth0 for all users.
func (am *Auth0Manager) createExportUsersJob(accountId string) error {
jwtToken, err := am.credentials.Authenticate()
if err != nil {
return err
}
reqURL := am.authIssuer + "/api/v2/jobs/users-exports"
payloadString := fmt.Sprintf("{\"format\": \"json\"," +
"\"fields\": [{\"name\": \"created_at\"}, {\"name\": \"last_login\"},{\"name\": \"user_id\"}, {\"name\": \"email\"}, {\"name\": \"name\"}, {\"name\": \"app_metadata.wt_account_id\", \"export_as\": \"wt_account_id\"}]}")
payload := strings.NewReader(payloadString)
exportJobReq, err := http.NewRequest("POST", reqURL, payload)
if err != nil {
return err
}
exportJobReq.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
exportJobReq.Header.Add("content-type", "application/json")
jobResp, err := am.httpClient.Do(exportJobReq)
if err != nil {
log.Debugf("Couldn't get job response %v", err)
return err
}
defer func() {
err = jobResp.Body.Close()
if err != nil {
log.Errorf("error while closing update user app metadata response body: %v", err)
}
}()
if jobResp.StatusCode != 200 {
return fmt.Errorf("unable to update the appMetadata, statusCode %d", jobResp.StatusCode)
}
var exportJobResp UserExportJobResponse
body, err := ioutil.ReadAll(jobResp.Body)
if err != nil {
log.Debugf("Coudln't read export job response; %v", err)
return err
}
err = am.helper.Unmarshal(body, &exportJobResp)
if err != nil {
log.Debugf("Coudln't unmarshal export job response; %v", err)
return err
}
if exportJobResp.Id == "" {
return fmt.Errorf("couldn't get an batch id status %d, %s, response body: %v", jobResp.StatusCode, jobResp.Status, exportJobResp)
}
log.Debugf("batch id status %d, %s, response body: %v", jobResp.StatusCode, jobResp.Status, exportJobResp)
ctx, cancel := context.WithTimeout(context.TODO(), 90*time.Second)
defer cancel()
done, downloadLink, err := am.checkExportJobStatus(ctx, exportJobResp.Id)
if err != nil {
log.Debugf("Failed at getting status checks from exportJob; %v", err)
return err
}
if done {
err = am.cacheUsers(downloadLink)
if err != nil {
log.Debugf("Failed to cache users via download link; %v", err)
}
}
return nil
}
// Downloads the users from auth0 and caches it in memory
// Users are only cached if they have an wt_account_id stored in auth0
func (am *Auth0Manager) cacheUsers(location string) error {
body, err := doGetReq(am.httpClient, location, "")
if err != nil {
log.Debugf("Can't download cached users; %v", err)
return err
}
bodyReader := bytes.NewReader(body)
gzipReader, err := gzip.NewReader(bodyReader)
if err != nil {
return err
}
decoder := json.NewDecoder(gzipReader)
for decoder.More() {
profile := Auth0Profile{}
err = decoder.Decode(&profile)
if err != nil {
log.Errorf("Couldn't decode profile; %v", err)
return err
}
if profile.AccountId != "" {
am.cachedUsersByAccountId[profile.AccountId] = append(am.cachedUsersByAccountId[profile.AccountId], profile)
}
}
return nil
}
// This checks the status of the job created at CreateExportUsersJob.
// If the status is "completed", then return the downloadLink
func (am *Auth0Manager) checkExportJobStatus(ctx context.Context, jobId string) (bool, string, error) {
retry := time.NewTicker(time.Second)
for {
select {
case <-ctx.Done():
log.Debugf("Export job status stopped...\n")
return false, "", ctx.Err()
case <-retry.C:
jwtToken, err := am.credentials.Authenticate()
if err != nil {
return false, "", err
}
statusUrl := am.authIssuer + "/api/v2/jobs/" + jobId
body, err := doGetReq(am.httpClient, statusUrl, jwtToken.AccessToken)
if err != nil {
return false, "", err
}
var status ExportJobStatusResponse
err = am.helper.Unmarshal(body, &status)
if err != nil {
return false, "", err
}
log.Debugf("Current export job status is %v", status.Status)
if status.Status != "completed" {
continue
}
return true, status.Location, nil
}
}
}
// Invalidates old cache for Account and re-queries it from auth0
func (am *Auth0Manager) forceUpdateUserCache(accountId string) error {
jwtToken, err := am.credentials.Authenticate()
if err != nil {
return err
}
var list []Auth0Profile
// https://auth0.com/docs/manage-users/user-search/retrieve-users-with-get-users-endpoint#limitations
// auth0 limitation of 1000 users via this endpoint
for page := 0; page < 20; page++ {
reqURL, query, err := batchRequestUsersUrl(am.authIssuer, accountId, page)
if err != nil {
return nil, err
return err
}
req, err := http.NewRequest(http.MethodGet, reqURL, strings.NewReader(query.Encode()))
if err != nil {
return nil, err
return err
}
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
@@ -231,41 +423,42 @@ func (am *Auth0Manager) GetBatchedUserData(accountId string) ([]*UserData, error
res, err := am.httpClient.Do(req)
if err != nil {
return nil, err
return err
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
return err
}
var batch []UserData
var batch []Auth0Profile
err = json.Unmarshal(body, &batch)
if err != nil {
return nil, err
return err
}
log.Debugf("requested batch; %v", batch)
err = res.Body.Close()
if err != nil {
return nil, err
return err
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("unable to request UserData from auth0, statusCode %d", res.StatusCode)
return fmt.Errorf("unable to request UserData from auth0, statusCode %d", res.StatusCode)
}
if len(batch) == 0 {
return list, nil
return nil
}
for user := range batch {
list = append(list, &batch[user])
list = append(list, batch[user])
}
}
am.cachedUsersByAccountId[accountId] = list
return list, nil
return nil
}
// GetUserDataByID requests user data from auth0 via ID
@@ -359,3 +552,54 @@ func (am *Auth0Manager) UpdateUserAppMetadata(userId string, appMetadata AppMeta
return nil
}
func batchRequestUsersUrl(authIssuer, accountId string, page int) (string, url.Values, error) {
u, err := url.Parse(authIssuer + "/api/v2/users")
if err != nil {
return "", nil, err
}
q := u.Query()
q.Set("page", strconv.Itoa(page))
q.Set("search_engine", "v3")
q.Set("q", "app_metadata.wt_account_id:"+accountId)
u.RawQuery = q.Encode()
return u.String(), q, nil
}
func requestByUserIdUrl(authIssuer, userId string) string {
return authIssuer + "/api/v2/users/" + userId
}
// Boilerplate implementation for Get Requests.
func doGetReq(client ManagerHTTPClient, url, accessToken string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if accessToken != "" {
req.Header.Add("authorization", "Bearer "+accessToken)
}
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
err = res.Body.Close()
if err != nil {
log.Errorf("error while closing body for url %s: %v", url, err)
}
}()
if res.StatusCode != 200 {
return nil, fmt.Errorf("unable to get %s, statusCode %d", url, res.StatusCode)
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
return body, nil
}

View File

@@ -11,7 +11,7 @@ import (
type Manager interface {
UpdateUserAppMetadata(userId string, appMetadata AppMetadata) error
GetUserDataByID(userId string, appMetadata AppMetadata) (*UserData, error)
GetBatchedUserData(accountId string) ([]*UserData, error)
GetAllUsers(accountId string) ([]*UserData, error)
}
// Config an idp configuration struct to be loaded from management server's config file

View File

@@ -251,8 +251,10 @@ func Test_SyncProtocol(t *testing.T) {
t.Fatal("expecting SyncResponse to have NetworkMap with a non-nil PeerConfig")
}
if networkMap.GetPeerConfig().GetAddress() != "100.64.0.1/24" {
t.Fatal("expecting SyncResponse to have NetworkMap with a PeerConfig having valid Address")
expectedIPNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
ip, _, _ := net.ParseCIDR(networkMap.GetPeerConfig().GetAddress())
if !expectedIPNet.Contains(ip) {
t.Fatalf("expecting SyncResponse to have NetworkMap with a PeerConfig having valid IP address %s", networkMap.GetPeerConfig().GetAddress())
}
if networkMap.GetSerial() <= 0 {

View File

@@ -107,8 +107,6 @@ var _ = Describe("Management service", func() {
err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp)
Expect(err).NotTo(HaveOccurred())
Expect(resp.PeerConfig.Address).To(BeEquivalentTo("100.64.0.1/24"))
expectedSignalConfig := &mgmtProto.HostConfig{
Uri: "signal.wiretrustee.com:10000",
Protocol: mgmtProto.HostConfig_HTTP,
@@ -308,10 +306,10 @@ var _ = Describe("Management service", func() {
})
})
Context("when there are 50 peers registered under one account", func() {
Context("when there are 10 peers registered under one account", func() {
Context("when there are 10 more peers registered under the same account", func() {
Specify("all of the 50 peers will get updates of 10 newly registered peers", func() {
initialPeers := 20
Specify("all of the 10 peers will get updates of 10 newly registered peers", func() {
initialPeers := 10
additionalPeers := 10
var peers []wgtypes.Key
@@ -367,7 +365,7 @@ var _ = Describe("Management service", func() {
key, _ := wgtypes.GenerateKey()
loginPeerWithValidSetupKey(serverPubKey, key, client)
rand.Seed(time.Now().UnixNano())
n := rand.Intn(500)
n := rand.Intn(200)
time.Sleep(time.Duration(n) * time.Millisecond)
}

View File

@@ -1,16 +1,14 @@
package server
import (
"encoding/binary"
"fmt"
"github.com/c-robinson/iplib"
"github.com/rs/xid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"math/rand"
"net"
"sync"
)
var (
upperIPv4 = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 255, 255, 255, 255}
upperIPv6 = []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
"time"
)
type NetworkMap struct {
@@ -30,10 +28,19 @@ type Network struct {
}
// NewNetwork creates a new Network initializing it with a Serial=0
// It takes a random /16 subnet from 100.64.0.0/10 (64 different subnets)
func NewNetwork() *Network {
n := iplib.NewNet4(net.ParseIP("100.64.0.0"), 10)
sub, _ := n.Subnet(16)
s := rand.NewSource(time.Now().Unix())
r := rand.New(s)
intn := r.Intn(len(sub))
return &Network{
Id: xid.New().String(),
Net: net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}},
Net: sub[intn].IPNet,
Dns: "",
Serial: 0}
}
@@ -63,51 +70,60 @@ func (n *Network) Copy() *Network {
// AllocatePeerIP pics an available IP from an net.IPNet.
// This method considers already taken IPs and reuses IPs if there are gaps in takenIps
// E.g. if ipNet=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.5] then the result would be 100.30.0.2
// E.g. if ipNet=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.4] then the result would be 100.30.0.2 or 100.30.0.3
func AllocatePeerIP(ipNet net.IPNet, takenIps []net.IP) (net.IP, error) {
takenIpMap := make(map[string]net.IP)
takenIpMap[ipNet.IP.String()] = ipNet.IP
takenIPMap := make(map[string]struct{})
takenIPMap[ipNet.IP.String()] = struct{}{}
for _, ip := range takenIps {
takenIpMap[ip.String()] = ip
}
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); ip = GetNextIP(ip) {
if _, ok := takenIpMap[ip.String()]; !ok {
return ip, nil
}
takenIPMap[ip.String()] = struct{}{}
}
return nil, fmt.Errorf("failed allocating new IP for the ipNet %s and takenIps %s", ipNet.String(), takenIps)
ips, _ := generateIPs(&ipNet, takenIPMap)
if len(ips) == 0 {
return nil, status.Errorf(codes.OutOfRange, "failed allocating new IP for the ipNet %s - network is out of IPs", ipNet.String())
}
// pick a random IP
s := rand.NewSource(time.Now().Unix())
r := rand.New(s)
intn := r.Intn(len(ips))
return ips[intn], nil
}
// GetNextIP returns the next IP from the given IP address. If the given IP is
// the last IP of a v4 or v6 range, the same IP is returned.
// Credits to Cilium team.
// Copyright 2017-2020 Authors of Cilium
func GetNextIP(ip net.IP) net.IP {
if ip.Equal(upperIPv4) || ip.Equal(upperIPv6) {
return ip
// generateIPs generates a list of all possible IPs of the given network excluding IPs specified in the exclusion list
func generateIPs(ipNet *net.IPNet, exclusions map[string]struct{}) ([]net.IP, int) {
var ips []net.IP
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); incIP(ip) {
if _, ok := exclusions[ip.String()]; !ok && ip[3] != 0 {
ips = append(ips, copyIP(ip))
}
}
nextIP := make(net.IP, len(ip))
switch len(ip) {
case net.IPv4len:
ipU32 := binary.BigEndian.Uint32(ip)
ipU32++
binary.BigEndian.PutUint32(nextIP, ipU32)
return nextIP
case net.IPv6len:
ipU64 := binary.BigEndian.Uint64(ip[net.IPv6len/2:])
ipU64++
binary.BigEndian.PutUint64(nextIP[net.IPv6len/2:], ipU64)
if ipU64 == 0 {
ipU64 = binary.BigEndian.Uint64(ip[:net.IPv6len/2])
ipU64++
binary.BigEndian.PutUint64(nextIP[:net.IPv6len/2], ipU64)
} else {
copy(nextIP[:net.IPv6len/2], ip[:net.IPv6len/2])
}
return nextIP
// remove network address and broadcast address
lenIPs := len(ips)
switch {
case lenIPs < 2:
return ips, lenIPs
default:
return ip
return ips[1 : len(ips)-1], lenIPs - 2
}
}
func copyIP(ip net.IP) net.IP {
dup := make(net.IP, len(ip))
copy(dup, ip)
return dup
}
func incIP(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}

View File

@@ -0,0 +1,39 @@
package server
import (
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestNewNetwork(t *testing.T) {
network := NewNetwork()
// generated net should be a subnet of a larger 100.64.0.0/10 net
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
assert.Equal(t, ipNet.Contains(network.Net.IP), true)
}
func TestAllocatePeerIP(t *testing.T) {
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 255, 255, 0}}
var ips []net.IP
for i := 0; i < 253; i++ {
ip, err := AllocatePeerIP(ipNet, ips)
if err != nil {
t.Fatal(err)
}
ips = append(ips, ip)
}
assert.Len(t, ips, 253)
uniq := make(map[string]struct{})
for _, ip := range ips {
if _, ok := uniq[ip.String()]; !ok {
uniq[ip.String()] = struct{}{}
} else {
t.Errorf("found duplicate IP %s", ip.String())
}
}
}

View File

@@ -346,7 +346,10 @@ func (am *DefaultAccountManager) AddPeer(
}
network := account.Network
nextIp, _ := AllocatePeerIP(network.Net, takenIps)
nextIp, err := AllocatePeerIP(network.Net, takenIps)
if err != nil {
return nil, err
}
newPeer := &Peer{
Key: peer.Key,

View File

@@ -21,7 +21,7 @@
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
"Net": {
"IP": "100.64.0.0",
"Mask": "/8AAAA=="
"Mask": "//8AAA=="
},
"Dns": null
},